From 8c9bf04addf1c355ef59466ce436760f7490a6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 9 Aug 2026 11:20:24 +0200 Subject: [PATCH] Headless: force opaque PNG screenshots by default, add --screenshot-keep-alpha Games often use the framebuffer alpha channel for non-visual purposes, so saved PNGs could look fully transparent in image viewers. Force alpha to 255 when writing PNG screenshots unless --screenshot-keep-alpha is passed. The MSE comparison ignores alpha either way. --- Core/CmdLine.cpp | 1 + Core/CmdLine.h | 2 ++ docs/frametest.md | 4 ++++ headless/Compare.cpp | 7 +++++-- headless/Compare.h | 2 +- headless/Headless.cpp | 6 +++++- headless/README.md | 1 + 7 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 86ebb822f2..1ed72500e6 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -183,6 +183,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless}, {POFF(screenshotFilenameSave), CmdParamType::String, "screenshot-save", '\0', "Save rendered screenshot to specified path (PNG if the path ends in .png, BMP otherwise)", CmdLineMode::Headless}, {POFF(screenshotFilenameDiff), CmdParamType::String, "screenshot-diff", '\0', "Save a visual comparison image to FILE when comparing screenshots", CmdLineMode::Headless}, + {POFF(screenshotSaveKeepAlpha), CmdParamType::Bool, "screenshot-keep-alpha", '\0', "Preserve the alpha channel when saving PNG screenshots (default: alpha is forced to 255)", CmdLineMode::Headless}, {POFF(timeout), CmdParamType::Double, "timeout", '\0', "Set the timeout value", CmdLineMode::Headless}, {POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless}, {POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1:", CmdLineMode::Headless}, diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 211b5853f4..89fe4cc995 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -96,6 +96,8 @@ struct CommandLineOptions { std::optional screenshotFilename; std::optional screenshotFilenameSave; std::optional screenshotFilenameDiff; + // Headless: preserve the alpha channel when saving PNG screenshots. + std::optional screenshotSaveKeepAlpha; // Headless: mount an ISO/CSO on umd1:. std::optional mountIso; diff --git a/docs/frametest.md b/docs/frametest.md index d481ef4177..9f18e356c6 100644 --- a/docs/frametest.md +++ b/docs/frametest.md @@ -108,6 +108,10 @@ test is kept in `/logs/`. - PNG, 512×272 (480×272 display in a 512-wide framebuffer), stored top-down (row 0 = top of screen). The BMP output format is bottom-up per the BMP spec; the flip is applied only when writing BMPs. +- The alpha channel is forced to 255 when writing PNGs (games often use alpha + for non-visual purposes, which would otherwise produce transparent-looking + images); pass `--screenshot-keep-alpha` to the headless binary to preserve + it (e.g. via a variant). The MSE comparison ignores alpha either way. - Generated with `--graphics=software` they are fully deterministic: a subsequent run produces byte-identical output, so `maxMse` can be 0. - If rendering code changes the output, existing references may need diff --git a/headless/Compare.cpp b/headless/Compare.cpp index eee48b3051..359c766b95 100644 --- a/headless/Compare.cpp +++ b/headless/Compare.cpp @@ -459,7 +459,7 @@ bool ScreenshotComparer::SaveActualBitmap(const Path &resultFilename) { return false; } -bool ScreenshotComparer::SaveActualPNG(const Path &resultFilename) { +bool ScreenshotComparer::SaveActualPNG(const Path &resultFilename, bool keepAlpha) { std::vector rgba((size_t)stride_ * h_ * 4); const u32 *pixels = pixels_.data(); for (size_t i = 0; i < (size_t)stride_ * h_; ++i) { @@ -467,7 +467,10 @@ bool ScreenshotComparer::SaveActualPNG(const Path &resultFilename) { rgba[i * 4 + 0] = (p >> 16) & 0xFF; rgba[i * 4 + 1] = (p >> 8) & 0xFF; rgba[i * 4 + 2] = p & 0xFF; - rgba[i * 4 + 3] = (p >> 24) & 0xFF; + // Games often use the alpha channel for non-visual purposes, which + // results in fully transparent images in PNG viewers. By default, + // force alpha to 255 when writing the image. + rgba[i * 4 + 3] = keepAlpha ? ((p >> 24) & 0xFF) : 0xFF; } return pngSave(resultFilename, rgba.data(), stride_, h_, 4); } diff --git a/headless/Compare.h b/headless/Compare.h index a9240440a9..e8b4700a21 100644 --- a/headless/Compare.h +++ b/headless/Compare.h @@ -47,7 +47,7 @@ public: } bool SaveActualBitmap(const Path &filename); - bool SaveActualPNG(const Path &filename); + bool SaveActualPNG(const Path &filename, bool keepAlpha = false); bool SaveVisualComparisonPNG(const Path &filename); protected: diff --git a/headless/Headless.cpp b/headless/Headless.cpp index c2bd6c09da..34b4587103 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -71,6 +71,7 @@ static Path g_comparisonScreenshot; static Path g_screenshotSavePath; static Path g_screenshotDiffPath; +static bool g_screenshotSaveKeepAlpha = false; static double g_maxScreenshotError = 0.0; static bool g_screenshotFailed = false; static std::string g_debugOutputBuffer; @@ -202,7 +203,7 @@ void System_SendDebugScreenshot(const uint8_t *data, int width, int height) { // If a screenshot save path is set, save unconditionally. if (!g_screenshotSavePath.empty()) { ScreenshotComparer saver(pixels, FRAME_STRIDE, FRAME_WIDTH, FRAME_HEIGHT); - bool saved = g_screenshotSavePath.GetFileExtension() == ".png" ? saver.SaveActualPNG(g_screenshotSavePath) : saver.SaveActualBitmap(g_screenshotSavePath); + bool saved = g_screenshotSavePath.GetFileExtension() == ".png" ? saver.SaveActualPNG(g_screenshotSavePath, g_screenshotSaveKeepAlpha) : saver.SaveActualBitmap(g_screenshotSavePath); if (saved) SendAndCollectOutput("Screenshot saved to: " + g_screenshotSavePath.ToVisualString() + "\n"); } @@ -782,6 +783,9 @@ int main(int argc, const char* argv[]) { if (cmdLineOptions.screenshotFilenameDiff.has_value()) { g_screenshotDiffPath = Path(std::string(cmdLineOptions.screenshotFilenameDiff.value())); } + if (cmdLineOptions.screenshotSaveKeepAlpha.has_value()) { + g_screenshotSaveKeepAlpha = cmdLineOptions.screenshotSaveKeepAlpha.value(); + } SetWriteFailureScreenshot(!getenv("GITHUB_ACTIONS") && !testOptions.bench); SetWriteDebugOutput(!testOptions.compare && !testOptions.bench); diff --git a/headless/README.md b/headless/README.md index 8e13caed3b..c6a0143e22 100644 --- a/headless/README.md +++ b/headless/README.md @@ -45,6 +45,7 @@ PPSSPPHeadless file.elf|file.prx|file.ppdmp [...] [options] | `--screenshot=` | Compare the rendered output against a reference screenshot. | | `--screenshot-save=` | Save the rendered output to a file (PNG if the path ends in `.png`, BMP otherwise). | | `--screenshot-diff=` | When comparing screenshots, save a visual comparison image to this file (always, regardless of pass/fail). | +| `--screenshot-keep-alpha` | Preserve the alpha channel when saving PNG screenshots (default: alpha is forced to 255, since games often use it for non-visual purposes). | | `--max-mse=` | Maximum allowed Mean Squared Error for screenshot comparison (default: 0 = exact). | | `--compare` / `-c` | Compare test output with `.expected` text file and/or screenshot (see below). | | `--timeout=` | Abort test if it takes longer than this. |