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
+2
View File
@@ -599,3 +599,5 @@
{"date": "2026-09-30T03:32:49+00:00", "unit": "UserInterface/PythonApplicationLogo.cpp", "action": "correction: N_A: CPythonApplication::OnLogo* has no caller: intrologo.py calls app.OnLogoOpen/Update/Render/Close, but those bind appLogo* in PythonApplicationModule.cpp, whose CPythonApplication::OnLogo* calls are commented out", "evidence": "LC_ALL=C grep app.OnLogo over the root .py (intrologo.py); PythonApplicationModule.cpp:1165-1195 (the earlier grep ran without LC_ALL=C and missed the CP949 scripts)"}
{"date": "2026-09-30T03:32:49+00:00", "unit": "UserInterface/MovieMan.cpp", "action": "correction: N_A: reached at boot through prototype.py SetLogoPhase -> intrologo.py app.OnLogoOpen(\"logo1.avi\"/\"logo2.avi\") -> CMovieMan::PlayLogo -> PlayMovie(<cwd>\\\\logoN.avi), but the 40250 Client ships no .avi, so RenderFileToMMStream fails and nothing is shown; PlayIntro/PlayTutorial have no caller. The port stub matches that outcome", "evidence": "find Client -iname *.avi: none; LC_ALL=C grep PlayIntro/PlayTutorial over ClientVS22/source (the earlier grep ran without LC_ALL=C and missed the CP949 scripts)"}
{"date": "2026-09-30T03:32:49+00:00", "unit": "UserInterface/PythonApplicationWebPage.cpp", "action": "correction: DIVERGENT: reachable: game.py __InGameShop_Show -> interfacemodule.OpenWebWindow -> uiweb.py app.ShowWebPage when constInfo.IN_GAME_SHOP_ENABLE (localeinfo sets it per locale), and uishop.MallPageDialog; the port has no embedded browser, so ShowWebPage/MoveWebPage/HideWebPage are stubs in platform/UserInterface/PythonApplication.cpp", "evidence": "LC_ALL=C grep ShowWebPage/uiweb/IN_GAME_SHOP_ENABLE over the root .py (the earlier grep ran without LC_ALL=C and missed the CP949 scripts)"}
{"date": "2026-09-30", "unit": "EterLib/JpegFile.cpp", "action": "jpeg_save ADAPTED (stb_image_write), destination managers + unused load/save_to_mem N_A", "evidence": "tests/port/port_screenshot_test.cpp; only CPythonGraphic::SaveJPEG calls jpeg_save"}
{"date": "2026-09-30", "unit": "EterPythonLib/PythonGraphic.cpp", "action": "GenScreenShotTag/SaveJPEG PORTED, SaveScreenShot ADAPTED (next-frame readback via ScreenShotHost)", "evidence": "port.screenshot; desktop grp.SaveScreenShot() -> ~/Documents/METIN2/0930_124051.jpg, colours checked"}
+78
View File
@@ -0,0 +1,78 @@
{
"reference": "EterLib/JpegFile.cpp",
"reference_sha256": "5e4417d5dfe77c4921e22b3196ca894a40034e41a19936fc49b509ad2c19b98a",
"priority": "P4",
"contracts": [],
"functions": {
"file_init_destination": {
"status": "N_A",
"note": "libjpeg stdio destination manager behind jpeg_save; stb_image_write writes through its own callback (see jpeg_save ADAPTED)."
},
"file_empty_output_buffer": {
"status": "N_A",
"note": "libjpeg stdio destination manager behind jpeg_save; stb_image_write writes through its own callback (see jpeg_save ADAPTED)."
},
"file_term_destination": {
"status": "N_A",
"note": "libjpeg stdio destination manager behind jpeg_save; stb_image_write writes through its own callback (see jpeg_save ADAPTED)."
},
"mem_init_destination": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_empty_output_buffer": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_term_destination": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"jpeg_save": {
"status": "ADAPTED",
"impl": [
"src/platform/EterLib/JpegFile.cpp:jpeg_save"
],
"test": [
"tests/port/port_screenshot_test.cpp"
],
"note": "40250 encodes with libjpeg-9a (stdio destination, JCS_RGB, jpeg_set_defaults + jpeg_set_quality(quality)); the port encodes the same top-down RGB rows at the same quality with stb_image_write into a baseline JFIF file. Invariant: returns 1 with a complete JPEG (SOI..EOI) at `filename`, 0 when the file cannot be opened. '\\\\' in the path is taken as a separator like the Win32 CRT fopen."
},
"jpeg_save_to_file": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"jpeg_save_to_mem": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_init_source": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_fill_input_buffer": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_skip_input_data": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_resync_to_restart": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"mem_term_source": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"jpeg_load_from_mem": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
},
"jpeg_load": {
"status": "N_A",
"note": "No caller in 40250 (LC_ALL=C grep -a over ClientVS22/source and the root .py: only CPythonGraphic::SaveJPEG calls jpeg_save); left undefined."
}
}
}
@@ -63,13 +63,32 @@
"note": "Verbatim 40250. The recording device reports no D3DCAPS2_FULLSCREENGAMMA (windowed output, where D3D8 ignores the ramp as well), so it returns at the caps check; IDirect3DDevice8::SetGammaRamp added to the D3D8 shim."
},
"GenScreenShotTag": {
"status": "TODO"
"status": "PORTED",
"impl": [
"src/platform/EterPythonLib/PythonGraphic.cpp:GenScreenShotTag"
],
"note": "Verbatim 40250; only reached when g_isScreenShotKey (false, never set)."
},
"CPythonGraphic::SaveJPEG": {
"status": "TODO"
"status": "PORTED",
"impl": [
"src/platform/EterPythonLib/PythonGraphic.cpp:CPythonGraphic::SaveJPEG"
],
"test": [
"tests/port/port_screenshot_test.cpp"
]
},
"CPythonGraphic::SaveScreenShot": {
"status": "TODO"
"status": "ADAPTED",
"impl": [
"src/platform/EterPythonLib/PythonGraphic.cpp:CPythonGraphic::SaveScreenShot",
"src/platform/EterPythonLib/PythonGraphic.cpp:ScreenShotHost::WriteScreenShot",
"src/host/live_client.cpp"
],
"test": [
"tests/port/port_screenshot_test.cpp"
],
"note": "40250 locks the D3D8 back buffer and writes the JPEG before returning. The script thread and the SDL host alternate, so the call queues a readback of the next presented frame (ScreenShotHost::SetRequest -> VulkanWindow::request_frame) and returns true; ScreenShotHost::WriteScreenShot runs the rest verbatim one frame later (X8R8G8B8 row loop, SaveJPEG, TraceError on failure, g_isScreenShotKey EXIF block). No host -> TraceError + false (40250's GetBackBuffer failure). Only the X8R8G8B8 branch exists: the swapchain is read back as BGRA. Checked on desktop: grp.SaveScreenShot() wrote ~/Documents/METIN2/<MMDD_hhmmss>.jpg with correct colours (SHGetSpecialFolderPath shim = $HOME/Documents, else cwd)."
},
"CPythonGraphic::PushState": {
"status": "PORTED",
+7
View File
@@ -13,6 +13,7 @@
#include "platform/PackBackend.h"
#include "platform/ScriptLib/PythonBoot.h"
#include "platform/EterPythonLib/ScreenShotHost.h"
#include "platform/UserInterface/ServerClock.h"
#include "platform/EterBase/TraceErrorObserver.h"
#include "platform/EterBase/FrameRateMode.h"
@@ -188,6 +189,11 @@ int run_live_client(
SDL_Log("live stage: PythonBoot started");
#endif
ScreenShotHost::SetRequest([&renderer](const std::string& path) {
return renderer.request_frame([path](const std::uint8_t* bgra, std::uint32_t width, std::uint32_t height) {
ScreenShotHost::WriteScreenShot(path.c_str(), bgra, width, height, width * 4);
});
});
SetPlatformServerTime(123456789);
PythonBoot::SetUISafeInset(renderer.ui_safe_inset());
PythonBoot::SetUISize(static_cast<int>(renderer.logical_width()), static_cast<int>(renderer.logical_height()));
@@ -505,6 +511,7 @@ int run_live_client(
const auto wall_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
print_summary(renderer, completed, wall_ms, update_ms, std::move(frame_ms));
ScreenShotHost::SetRequest(nullptr);
PythonBoot::Stop();
#ifdef __ANDROID__
SDL_Log("live stage: PythonBoot stopped");
+6
View File
@@ -139,6 +139,12 @@ void VulkanWindow::request_screenshot(std::string path, std::uint64_t frame) {
screenshot_frame_ = frame;
}
bool VulkanWindow::request_frame(FrameSink sink) {
if (!swapchain_readable_) return false;
frame_sinks_.push_back(std::move(sink));
return true;
}
std::uint32_t VulkanWindow::logical_width() const {
int w = 0, h = 0;
if (window_) SDL_GetWindowSize(window_, &w, &h);
+1
View File
@@ -120,6 +120,7 @@ void VulkanWindow::create_swapchain() {
info.imageFormat = format.format; info.imageColorSpace = format.colorSpace; info.imageExtent = extent_;
info.imageArrayLayers = 1; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
(capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
swapchain_readable_ = (info.imageUsage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0;
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
// Android surfaces can report a quarter-turn transform even when SDL's window is
// already landscape. Rendering in SDL window coordinates and requesting that transform
+13 -3
View File
@@ -493,7 +493,9 @@ void VulkanWindow::record_passes(std::uint32_t image_index, const FrameDraws& fr
VulkanWindow::Readback VulkanWindow::record_readbacks(std::uint32_t image_index) {
Readback readback;
readback.screenshot = !screenshot_path_.empty() && frame_number_ == screenshot_frame_;
readback.bmp = !screenshot_path_.empty() && frame_number_ == screenshot_frame_;
readback.sinks.swap(frame_sinks_);
readback.screenshot = readback.bmp || !readback.sinks.empty();
if (readback.screenshot) {
const VkDeviceSize bytes = VkDeviceSize(extent_.width) * extent_.height * 4;
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
@@ -530,7 +532,7 @@ VulkanWindow::Readback VulkanWindow::record_readbacks(std::uint32_t image_index)
}
// Debug: MT_DUMP_RT=prefix writes each render target of the screenshot frame to <prefix>_<n>.pgm.
const char* dump_rt = std::getenv("MT_DUMP_RT");
if (readback.screenshot && dump_rt && *dump_rt) {
if (readback.bmp && dump_rt && *dump_rt) {
readback.dump_rt = dump_rt;
const std::uint32_t texel = offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
for (auto& entry : render_targets_) {
@@ -578,7 +580,15 @@ void VulkanWindow::finish_readbacks(Readback& readback) {
check(vkWaitForFences(device_, 1, &f_->fence, VK_TRUE, UINT64_MAX), "vkWaitForFences readback");
void* mapped = nullptr;
check(vkMapMemory(device_, readback.memory, 0, VK_WHOLE_SIZE, 0, &mapped), "vkMapMemory readback");
write_bmp(screenshot_path_, static_cast<const std::uint8_t*>(mapped));
if (readback.bmp) write_bmp(screenshot_path_, static_cast<const std::uint8_t*>(mapped));
if (!readback.sinks.empty()) {
const std::uint32_t w = extent_.width, h = extent_.height;
std::vector<std::uint8_t> bgra(static_cast<const std::uint8_t*>(mapped),
static_cast<const std::uint8_t*>(mapped) + std::size_t(w) * h * 4);
if (swapchain_format_ == VK_FORMAT_R8G8B8A8_UNORM || swapchain_format_ == VK_FORMAT_R8G8B8A8_SRGB)
for (std::size_t i = 0; i < bgra.size(); i += 4) std::swap(bgra[i], bgra[i + 2]);
for (auto& sink : readback.sinks) sink(bgra.data(), w, h);
}
vkUnmapMemory(device_, readback.memory);
const std::uint32_t texel = offscreen_format_ == VK_FORMAT_R5G6B5_UNORM_PACK16 ? 2 : 4;
for (std::size_t n = 0; n < readback.rt_dumps.size(); ++n) {
+10 -1
View File
@@ -13,6 +13,7 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
@@ -133,6 +134,10 @@ public:
double input_idle_seconds() const;
void request_screenshot(std::string path, std::uint64_t frame);
// Hands the next presented frame to `sink` as top-down BGRA rows (grp.SaveScreenShot). False when the
// swapchain images cannot be copied from.
using FrameSink = std::function<void(const std::uint8_t* bgra, std::uint32_t width, std::uint32_t height)>;
bool request_frame(FrameSink sink);
std::uint64_t frame_number() const { return frame_number_; }
int msaa_samples() const { return int(samples_); }
@@ -215,7 +220,9 @@ private:
// Host copies of the swapchain image (--screenshot-out) and, with MT_DUMP_RT, of every render target.
struct Readback {
struct RtDump { VkBuffer buffer; VkDeviceMemory memory; std::uint32_t w, h; };
bool screenshot = false;
bool screenshot = false; // the swapchain image is copied (a BMP or a frame sink wants it)
bool bmp = false; // --screenshot-out
std::vector<FrameSink> sinks;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
const char* dump_rt = nullptr;
@@ -398,6 +405,8 @@ private:
// --screenshot-out: read back the swapchain image of one frame into a BMP file.
std::string screenshot_path_;
std::uint64_t screenshot_frame_ = 0;
std::vector<FrameSink> frame_sinks_; // request_frame, served by the next frame's readback
bool swapchain_readable_ = false; // the swapchain images have TRANSFER_SRC usage
VkImage depth_image_ = VK_NULL_HANDLE;
VkDeviceMemory depth_memory_ = VK_NULL_HANDLE;
VkImageView depth_view_ = VK_NULL_HANDLE;
+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
+5
View File
@@ -209,6 +209,11 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
target_link_libraries(port_spherelib_test PRIVATE port_platform)
add_test(NAME port.spherelib COMMAND $<TARGET_FILE:port_spherelib_test>)
# grp.SaveScreenShot: the host frame request and the 40250 JPEG write behind it.
add_executable(port_screenshot_test ${PROJECT_SOURCE_DIR}/tests/port/port_screenshot_test.cpp)
target_link_libraries(port_screenshot_test PRIVATE port_platform)
add_test(NAME port.screenshot COMMAND $<TARGET_FILE:port_screenshot_test>)
# 批次 2V1: Packet.h struct sizes against the classic wire layouts.
add_executable(port_packet_test ${PROJECT_SOURCE_DIR}/tests/port/port_packet_test.cpp
${PROJECT_SOURCE_DIR}/tests/port/port_packet_test_wire.cpp)
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <stdio.h>
int jpeg_save(unsigned char*data_d, int width, int height, int quality, const char*filename);
int jpeg_save_to_file(unsigned char*data_d, int width, int height, int quality, FILE*fi);
int jpeg_save_to_mem(unsigned char*data_d, int width, int height, int quality, unsigned char*dest, int destsize);
int jpeg_load(const char*filename, unsigned char**dest, int*width, int*height);
int jpeg_load_from_mem(unsigned char*_data, int size_d, unsigned char*dest, int width, int height);
+11 -2
View File
@@ -32,9 +32,18 @@ void DeleteCriticalSection(LPCRITICAL_SECTION cs) { cs_mutex(cs).~recursive_mute
void EnterCriticalSection(LPCRITICAL_SECTION cs) { cs_mutex(cs).lock(); }
void LeaveCriticalSection(LPCRITICAL_SECTION cs) { cs_mutex(cs).unlock(); }
// Win32 takes '\\' and '/' as separators alike.
static std::string native_path(const char* path)
{
std::string normalized(path ? path : "");
for (char& ch : normalized)
if (ch == '\\') ch = '/';
return normalized;
}
BOOL CreateDirectory(LPCSTR path, void*)
{
return mkdir(path, 0777) == 0 ? TRUE : FALSE;
return mkdir(native_path(path).c_str(), 0777) == 0 ? TRUE : FALSE;
}
BOOL DeleteFile(LPCSTR path)
@@ -49,7 +58,7 @@ int _access(const char* path, int mode)
m |= W_OK;
if (mode & 4)
m |= R_OK;
return access(path, m);
return access(native_path(path).c_str(), m);
}
int _unlink(const char* path)
+24 -2
View File
@@ -2,11 +2,33 @@
// Win32 shell-folder API used only by the screenshot command in grp.
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <sys/stat.h>
#include <unistd.h>
#define CSIDL_PERSONAL 0x0005
// CSIDL_PERSONAL ("My Documents"): $HOME/Documents where it exists (desktop), otherwise the working
// directory, which is the writable Client directory on Android. grp.SaveScreenShot appends
// "\\METIN2\\"; CreateDirectory and jpeg_save take '\\' as a separator like Win32 does.
inline BOOL SHGetSpecialFolderPath(void*, char* path, int, BOOL)
{
if (path)
if (!path)
return FALSE;
path[0] = '\0';
struct stat st;
const char* home = std::getenv("HOME");
if (home && home[0])
{
std::snprintf(path, MAX_PATH, "%s/Documents", home);
if (::stat(path, &st) == 0 && S_ISDIR(st.st_mode))
return TRUE;
}
if (!::getcwd(path, MAX_PATH))
{
path[0] = '\0';
return FALSE;
return FALSE;
}
return TRUE;
}
+78
View File
@@ -0,0 +1,78 @@
// grp.SaveScreenShot: CPythonGraphic::SaveScreenShot queues a host frame request (ScreenShotHost.h) and
// ScreenShotHost::WriteScreenShot runs the rest of 40250's SaveScreenShot on the frame: X8R8G8B8 rows ->
// RGB -> SaveJPEG (jpeg_save, quality 100).
#include "EterPythonLib/StdAfx.h"
#include "EterPythonLib/PythonGraphic.h"
#include "../../src/platform/EterPythonLib/ScreenShotHost.h"
#include <cstdio>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <vector>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static std::vector<unsigned char> ReadFile(const std::string& path)
{
std::vector<unsigned char> data;
if (FILE* f = std::fopen(path.c_str(), "rb")) {
int c;
while ((c = std::fgetc(f)) != EOF)
data.push_back(static_cast<unsigned char>(c));
std::fclose(f);
}
return data;
}
int main()
{
CPythonGraphic graphic;
// No host: 40250's GetBackBuffer failure path.
ScreenShotHost::SetRequest(nullptr);
CHECK(!graphic.SaveScreenShot("unused.jpg"));
char dir[] = "/tmp/mt_screenshot_XXXXXX";
CHECK(mkdtemp(dir) != nullptr);
const std::string path = std::string(dir) + "/shot.jpg";
std::vector<std::string> requested;
ScreenShotHost::SetRequest([&](const std::string& p) { requested.push_back(p); return true; });
CHECK(graphic.SaveScreenShot(path.c_str()));
CHECK(requested.size() == 1 && requested[0] == path);
CHECK(ReadFile(path).empty()); // written when the frame arrives, not by the call
// 8x4 frame with a padded pitch; the padding bytes must not reach the image.
const unsigned w = 8, h = 4, pitch = w * 4 + 16;
std::vector<std::uint8_t> frame(pitch * h, 0xAB);
for (unsigned y = 0; y < h; ++y)
for (unsigned x = 0; x < w; ++x) {
std::uint8_t* px = &frame[y * pitch + x * 4];
px[0] = 0x20; px[1] = 0x80; px[2] = 0xE0; px[3] = 0xFF; // B, G, R, X
}
CHECK(ScreenShotHost::WriteScreenShot(requested[0].c_str(), frame.data(), w, h, pitch));
const std::vector<unsigned char> jpeg = ReadFile(path);
CHECK(jpeg.size() > 4);
CHECK(jpeg.size() > 4 && jpeg[0] == 0xFF && jpeg[1] == 0xD8);
CHECK(jpeg.size() > 4 && jpeg[jpeg.size() - 2] == 0xFF && jpeg[jpeg.size() - 1] == 0xD9);
// An unwritable path is SaveJPEG's failure.
CHECK(!ScreenShotHost::WriteScreenShot((std::string(dir) + "/missing/shot.jpg").c_str(), frame.data(), w, h, pitch));
ScreenShotHost::SetRequest(nullptr);
std::remove(path.c_str());
rmdir(dir);
if (g_failures)
std::fprintf(stderr, "port_screenshot_test: %d failure(s)\n", g_failures);
else
std::puts("port_screenshot_test: ok");
return g_failures ? 1 : 0;
}