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
+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;