yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
screenshot_utils.cc
Go to the documentation of this file.
2
3#ifdef YAZE_WITH_GRPC
4
6
7// SDL includes Windows headers whose macros conflict with protobuf/Abseil.
8#ifdef _WIN32
9#ifdef DWORD
10#undef DWORD
11#endif
12#ifdef ERROR
13#undef ERROR
14#endif
15#ifdef OVERFLOW
16#undef OVERFLOW
17#endif
18#ifdef IGNORE
19#undef IGNORE
20#endif
21#endif
22
23#include <algorithm>
24#include <cmath>
25#include <cstdio>
26#include <cstring>
27#include <filesystem>
28#include <limits>
29#include <memory>
30#include <string>
31#include <vector>
32
33#ifdef YAZE_SCREENSHOT_HAS_PNG
34#include <png.h>
35#endif
36
37#include "absl/status/status.h"
38#include "absl/strings/ascii.h"
39#include "absl/strings/str_format.h"
40#include "absl/time/clock.h"
41#include "imgui.h"
42#include "imgui_internal.h"
43#include "util/macro.h"
44#include "util/platform_paths.h"
45
46namespace yaze::test {
47namespace {
48
49using SurfacePtr =
50 std::unique_ptr<SDL_Surface, decltype(&platform::DestroySurface)>;
51
52const char* ExtensionForFormat(ScreenshotFormat format) {
53 return format == ScreenshotFormat::kPng ? ".png" : ".bmp";
54}
55
56absl::StatusOr<SDL_Renderer*> GetScreenshotRenderer() {
57 if (!ImGui::GetCurrentContext()) {
58 return absl::FailedPreconditionError("No ImGui context");
59 }
60 const ImGuiIO& io = ImGui::GetIO();
61#ifdef YAZE_USE_SDL3
62 constexpr const char* kBackendName = "imgui_impl_sdlrenderer3";
63#else
64 constexpr const char* kBackendName = "imgui_impl_sdlrenderer2";
65#endif
66 if (!io.BackendRendererName ||
67 std::strcmp(io.BackendRendererName, kBackendName) != 0) {
68 return absl::UnimplementedError(
69 "Screenshot capture requires the SDL renderer backend");
70 }
71 if (!io.BackendRendererUserData) {
72 return absl::FailedPreconditionError("SDL renderer not available");
73 }
74 // Both supported ImGui SDL renderer backends store the main renderer as
75 // their first field. Check the backend above before reading that field;
76 // Metal/OpenGL backend data has a different layout. Copy avoids aliasing an
77 // unrelated private backend struct from this translation unit.
78 SDL_Renderer* renderer = nullptr;
79 std::memcpy(&renderer, io.BackendRendererUserData, sizeof(renderer));
80 if (!renderer) {
81 return absl::FailedPreconditionError("SDL renderer not available");
82 }
83 if (SDL_GetRenderTarget(renderer) != nullptr) {
84 return absl::FailedPreconditionError(
85 "Screenshot capture requires the main framebuffer render target");
86 }
87 return renderer;
88}
89
90absl::Status GetOutputSize(SDL_Renderer* renderer, int* width, int* height) {
91#ifdef YAZE_USE_SDL3
92 const bool success = SDL_GetCurrentRenderOutputSize(renderer, width, height);
93#else
94 const bool success = SDL_GetRendererOutputSize(renderer, width, height) == 0;
95#endif
96 if (!success) {
97 return absl::InternalError(
98 absl::StrFormat("Failed to get renderer size: %s", SDL_GetError()));
99 }
100 if (*width <= 0 || *height <= 0) {
101 return absl::FailedPreconditionError("Renderer has no visible framebuffer");
102 }
103 return absl::OkStatus();
104}
105
106absl::StatusOr<SDL_Rect> ClipRegion(const CaptureRegion& region, int width,
107 int height) {
108 if (region.width <= 0 || region.height <= 0) {
109 return absl::InvalidArgumentError("Invalid capture region");
110 }
111 const int64_t left = std::max<int64_t>(0, region.x);
112 const int64_t top = std::max<int64_t>(0, region.y);
113 const int64_t right =
114 std::min<int64_t>(width, static_cast<int64_t>(region.x) + region.width);
115 const int64_t bottom =
116 std::min<int64_t>(height, static_cast<int64_t>(region.y) + region.height);
117 if (right <= left || bottom <= top) {
118 return absl::InvalidArgumentError(
119 "Capture region is outside the framebuffer");
120 }
121 return SDL_Rect{static_cast<int>(left), static_cast<int>(top),
122 static_cast<int>(right - left),
123 static_cast<int>(bottom - top)};
124}
125
126absl::StatusOr<SurfacePtr> ReadFramebufferRegion(SDL_Renderer* renderer,
127 const SDL_Rect& region) {
128 // SDL2 intersects readback with the current renderer viewport, even when
129 // passed an explicit framebuffer rectangle. ImGui restores the caller's
130 // viewport after rendering, so read the full requested region temporarily.
131 SDL_Rect previous_viewport;
132#ifdef YAZE_USE_SDL3
133 if (!SDL_GetRenderViewport(renderer, &previous_viewport)) {
134 return absl::InternalError("Failed to get renderer viewport");
135 }
136 const bool viewport_was_set = SDL_RenderViewportSet(renderer);
137 const bool reset = SDL_SetRenderViewport(renderer, nullptr);
138#else
139 SDL_RenderGetViewport(renderer, &previous_viewport);
140 const bool reset = SDL_RenderSetViewport(renderer, nullptr) == 0;
141#endif
142 SurfacePtr surface(reset ? platform::ReadPixelsToSurface(renderer, region.w,
143 region.h, &region)
144 : nullptr,
145 platform::DestroySurface);
146#ifdef YAZE_USE_SDL3
147 const bool restored = SDL_SetRenderViewport(
148 renderer, viewport_was_set ? &previous_viewport : nullptr);
149#else
150 const bool restored =
151 SDL_RenderSetViewport(renderer, &previous_viewport) == 0;
152#endif
153 if (!restored) {
154 return absl::InternalError(absl::StrFormat(
155 "Failed to restore screenshot viewport: %s", SDL_GetError()));
156 }
157 if (!reset) {
158 return absl::InternalError(absl::StrFormat(
159 "Failed to reset screenshot viewport: %s", SDL_GetError()));
160 }
161 if (!surface) {
162 return absl::InternalError(absl::StrFormat(
163 "Failed to read pixels to surface: %s", SDL_GetError()));
164 }
165 return surface;
166}
167
168absl::StatusOr<CaptureRegion> WindowRegion(const ImGuiWindow& window) {
169 if (!window.Active || window.Hidden || window.Collapsed) {
170 return absl::FailedPreconditionError("Requested window is not visible");
171 }
172 const ImGuiViewport* viewport = ImGui::GetMainViewport();
173 if (window.Viewport != viewport) {
174 return absl::UnimplementedError(
175 "Detached window screenshots are not supported by the main renderer");
176 }
177 const ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale;
178 if (!std::isfinite(scale.x) || !std::isfinite(scale.y) || scale.x <= 0 ||
179 scale.y <= 0) {
180 return absl::FailedPreconditionError("Invalid framebuffer scale");
181 }
182 const double left = std::floor(
183 (static_cast<double>(window.Pos.x) - viewport->Pos.x) * scale.x);
184 const double top = std::floor(
185 (static_cast<double>(window.Pos.y) - viewport->Pos.y) * scale.y);
186 const double right = std::ceil(
187 (static_cast<double>(window.Pos.x) + window.Size.x - viewport->Pos.x) *
188 scale.x);
189 const double bottom = std::ceil(
190 (static_cast<double>(window.Pos.y) + window.Size.y - viewport->Pos.y) *
191 scale.y);
192 constexpr double kMinInt = std::numeric_limits<int>::min();
193 constexpr double kMaxInt = std::numeric_limits<int>::max();
194 if (!std::isfinite(left) || !std::isfinite(top) || !std::isfinite(right) ||
195 !std::isfinite(bottom) || left < kMinInt || top < kMinInt ||
196 right > kMaxInt || bottom > kMaxInt || right <= left || bottom <= top ||
197 right - left > kMaxInt || bottom - top > kMaxInt) {
198 return absl::InvalidArgumentError("Invalid window framebuffer bounds");
199 }
200 return CaptureRegion{static_cast<int>(left), static_cast<int>(top),
201 static_cast<int>(right - left),
202 static_cast<int>(bottom - top)};
203}
204
205absl::StatusOr<std::filesystem::path> ScreenshotPath(
206 const std::string& preferred_path, ScreenshotFormat format) {
207 std::filesystem::path path;
208 if (!preferred_path.empty()) {
209 path = preferred_path;
210 if (!path.has_extension()) {
211 path += ExtensionForFormat(format);
212 }
213 } else {
215 auto directory,
217 path =
218 directory /
219 absl::StrFormat("yaze_%lld%s",
220 static_cast<long long>(absl::ToUnixMillis(absl::Now())),
221 ExtensionForFormat(format));
222 }
223 std::error_code error;
224 auto absolute = std::filesystem::absolute(path, error);
225 if (error) {
226 return absl::InternalError(absl::StrFormat(
227 "Failed to resolve screenshot path: %s", error.message()));
228 }
229 return absolute;
230}
231
232absl::Status SaveSurface(SDL_Surface* surface,
233 const std::filesystem::path& path,
234 ScreenshotFormat format) {
235 if (format == ScreenshotFormat::kBmp) {
236#ifdef YAZE_USE_SDL3
237 const bool success = SDL_SaveBMP(surface, path.string().c_str());
238#else
239 const bool success = SDL_SaveBMP(surface, path.string().c_str()) == 0;
240#endif
241 return success ? absl::OkStatus()
242 : absl::InternalError(absl::StrFormat(
243 "Failed to save BMP: %s", SDL_GetError()));
244 }
245#ifdef YAZE_SCREENSHOT_HAS_PNG
246 SurfacePtr rgba(
247 platform::ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA32, 0),
249 if (!rgba) {
250 return absl::InternalError(absl::StrFormat(
251 "Failed to convert screenshot pixels: %s", SDL_GetError()));
252 }
253 // Construct C++ owners before setjmp; libpng's error jump must not bypass
254 // their initialization/destruction. RGBA32 defines byte order on any endian.
255 std::vector<png_bytep> rows(rgba->h);
256 for (int y = 0; y < rgba->h; ++y) {
257 rows[y] = static_cast<png_bytep>(rgba->pixels) +
258 static_cast<size_t>(y) * rgba->pitch;
259 }
260 FILE* file = std::fopen(path.string().c_str(), "wb");
261 if (!file) {
262 return absl::InternalError("Failed to open PNG output file");
263 }
264 png_structp png =
265 png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
266 png_infop info = png ? png_create_info_struct(png) : nullptr;
267 if (!png || !info) {
268 if (png) {
269 png_destroy_write_struct(&png, nullptr);
270 }
271 std::fclose(file);
272 return absl::InternalError("Failed to initialize PNG encoder");
273 }
274 if (setjmp(png_jmpbuf(png))) {
275 png_destroy_write_struct(&png, &info);
276 std::fclose(file);
277 return absl::InternalError("Failed to encode PNG screenshot");
278 }
279 png_init_io(png, file);
280 png_set_IHDR(png, info, rgba->w, rgba->h, 8, PNG_COLOR_TYPE_RGBA,
281 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
282 PNG_FILTER_TYPE_DEFAULT);
283 png_write_info(png, info);
284 png_write_image(png, rows.data());
285 png_write_end(png, nullptr);
286 png_destroy_write_struct(&png, &info);
287 if (std::fclose(file) != 0) {
288 return absl::InternalError("Failed to finish PNG screenshot file");
289 }
290 return absl::OkStatus();
291#else
292 return absl::UnimplementedError(
293 "PNG screenshot encoding unavailable (libpng missing)");
294#endif
295}
296
297void RevealScreenshot([[maybe_unused]] const std::filesystem::path& path) {
298#ifdef __APPLE__
299 // Use a file URL, not a shell command containing a caller-controlled path.
300 std::error_code error;
301 const auto absolute = std::filesystem::absolute(path, error);
302 if (error) {
303 return;
304 }
305 std::string url = "file://";
306 for (const unsigned char byte : absolute.generic_string()) {
307 if (absl::ascii_isalnum(byte) || byte == '/' || byte == '-' ||
308 byte == '_' || byte == '.' || byte == '~') {
309 url += byte;
310 } else {
311 url += absl::StrFormat("%%%02X", byte);
312 }
313 }
314 (void)SDL_OpenURL(url.c_str());
315#endif
316}
317
318} // namespace
319
320absl::StatusOr<ScreenshotFormat> ResolveScreenshotFormat(
321 const std::string& path, ScreenshotFormat format) {
322 if (format != ScreenshotFormat::kAuto && format != ScreenshotFormat::kPng &&
323 format != ScreenshotFormat::kBmp) {
324 return absl::InvalidArgumentError("Unknown screenshot format");
325 }
326 const std::filesystem::path output(path);
327 if (!path.empty() &&
328 (output.filename().empty() || output.filename() == "." ||
329 output.filename() == ".." || path.find('\0') != std::string::npos)) {
330 return absl::InvalidArgumentError("Screenshot path must name a file");
331 }
332 const std::string extension =
333 absl::AsciiStrToLower(output.extension().string());
335 if (extension == ".png") {
336 inferred = ScreenshotFormat::kPng;
337 } else if (extension == ".bmp") {
338 inferred = ScreenshotFormat::kBmp;
339 } else if (!extension.empty()) {
340 return absl::InvalidArgumentError(
341 "Screenshot extension must be .png or .bmp");
342 }
343 if (format == ScreenshotFormat::kAuto) {
344 format =
345 inferred == ScreenshotFormat::kAuto ? ScreenshotFormat::kBmp : inferred;
346 } else if (inferred != ScreenshotFormat::kAuto && inferred != format) {
347 return absl::InvalidArgumentError(
348 "Screenshot extension does not match requested format");
349 }
350#ifndef YAZE_SCREENSHOT_HAS_PNG
351 if (format == ScreenshotFormat::kPng) {
352 return absl::UnimplementedError(
353 "PNG screenshot encoding unavailable (libpng missing)");
354 }
355#endif
356 return format;
357}
358
359absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshot(
360 const std::string& preferred_path, bool reveal_to_user,
361 ScreenshotFormat format) {
362 return CaptureHarnessScreenshotRegion(std::nullopt, preferred_path,
363 reveal_to_user, format);
364}
365
366absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshotRegion(
367 const std::optional<CaptureRegion>& region,
368 const std::string& preferred_path, bool reveal_to_user,
369 ScreenshotFormat format) {
370 ASSIGN_OR_RETURN(const auto resolved,
371 ResolveScreenshotFormat(preferred_path, format));
372 ASSIGN_OR_RETURN(SDL_Renderer * renderer, GetScreenshotRenderer());
373 int width = 0;
374 int height = 0;
375 RETURN_IF_ERROR(GetOutputSize(renderer, &width, &height));
377 const auto clipped,
378 ClipRegion(region.value_or(CaptureRegion{0, 0, width, height}), width,
379 height));
380 ASSIGN_OR_RETURN(auto surface, ReadFramebufferRegion(renderer, clipped));
381 ASSIGN_OR_RETURN(const auto output, ScreenshotPath(preferred_path, resolved));
382 if (output.has_parent_path()) {
383 std::error_code error;
384 std::filesystem::create_directories(output.parent_path(), error);
385 if (error) {
386 return absl::InternalError(absl::StrFormat(
387 "Failed to create screenshot directory: %s", error.message()));
388 }
389 }
390 RETURN_IF_ERROR(SaveSurface(surface.get(), output, resolved));
391 std::error_code error;
392 const auto bytes = std::filesystem::file_size(output, error);
393 if (error) {
394 return absl::InternalError(absl::StrFormat(
395 "Failed to stat screenshot %s: %s", output.string(), error.message()));
396 }
397 ScreenshotArtifact artifact{output.string(), clipped.w, clipped.h,
398 static_cast<int64_t>(bytes)};
399 if (reveal_to_user) {
400 RevealScreenshot(output);
401 }
402 return artifact;
403}
404
405absl::StatusOr<ScreenshotArtifact> CaptureActiveWindow(
406 const std::string& preferred_path, bool reveal_to_user,
407 ScreenshotFormat format) {
408 const ImGuiContext* ctx = ImGui::GetCurrentContext();
409 if (!ctx || !ctx->NavWindow) {
410 return absl::FailedPreconditionError("No active ImGui window");
411 }
412 ASSIGN_OR_RETURN(const auto region, WindowRegion(*ctx->NavWindow));
413 return CaptureHarnessScreenshotRegion(region, preferred_path, reveal_to_user,
414 format);
415}
416
417absl::StatusOr<ScreenshotArtifact> CaptureWindowByName(
418 const std::string& window_name, const std::string& preferred_path,
419 bool reveal_to_user, ScreenshotFormat format) {
420 if (!ImGui::GetCurrentContext()) {
421 return absl::FailedPreconditionError("No ImGui context");
422 }
423 const ImGuiWindow* window = ImGui::FindWindowByName(window_name.c_str());
424 if (!window) {
425 return absl::NotFoundError(
426 absl::StrFormat("Window '%s' not found", window_name));
427 }
428 ASSIGN_OR_RETURN(const auto region, WindowRegion(*window));
429 return CaptureHarnessScreenshotRegion(region, preferred_path, reveal_to_user,
430 format);
431}
432
433} // namespace yaze::test
434
435#endif // YAZE_WITH_GRPC
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
void DestroySurface(SDL_Surface *surface)
Destroy a surface.
Definition sdl_compat.h:719
SDL_Surface * ConvertSurfaceFormat(SDL_Surface *surface, uint32_t format, uint32_t flags=0)
Convert a surface to a specific pixel format.
Definition sdl_compat.h:377
SDL_Surface * ReadPixelsToSurface(SDL_Renderer *renderer, int width, int height, const SDL_Rect *rect)
Read pixels from renderer to a surface.
Definition sdl_compat.h:748
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22