diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 8f3bc99562..063c71c4ba 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -64,6 +64,8 @@ add_library(common HttpRequest.h Image.cpp Image.h + ImageC.c + ImageC.h IniFile.cpp IniFile.h Inline.h diff --git a/Source/Core/Common/Image.cpp b/Source/Core/Common/Image.cpp index 3833dedb6f..3b5c644c4d 100644 --- a/Source/Core/Common/Image.cpp +++ b/Source/Core/Common/Image.cpp @@ -10,6 +10,9 @@ #include "Common/CommonTypes.h" #include "Common/IOFile.h" +#include "Common/ImageC.h" +#include "Common/Logging/Log.h" +#include "Common/Timer.h" namespace Common { @@ -39,23 +42,28 @@ bool LoadPNG(const std::vector& input, std::vector* data_out, u32* width return true; } -bool SavePNG(const std::string& path, const u8* input, ImageByteFormat format, u32 width, - u32 height, int stride) +static void WriteCallback(png_structp png_ptr, png_bytep data, size_t length) { - png_image png = {}; - png.version = PNG_IMAGE_VERSION; - png.width = width; - png.height = height; + std::vector* buffer = static_cast*>(png_get_io_ptr(png_ptr)); + buffer->insert(buffer->end(), data, data + length); +} + +bool SavePNG(const std::string& path, const u8* input, ImageByteFormat format, u32 width, + u32 height, int stride, int level) +{ + Common::Timer timer; + timer.Start(); size_t byte_per_pixel; + int png_format; switch (format) { case ImageByteFormat::RGB: - png.format = PNG_FORMAT_RGB; + png_format = PNG_FORMAT_RGB; byte_per_pixel = 3; break; case ImageByteFormat::RGBA: - png.format = PNG_FORMAT_RGBA; + png_format = PNG_FORMAT_RGBA; byte_per_pixel = 4; break; default: @@ -64,30 +72,47 @@ bool SavePNG(const std::string& path, const u8* input, ImageByteFormat format, u // libpng doesn't handle non-ASCII characters in path, so write in two steps: // first to memory, then to file - std::vector buffer(byte_per_pixel * width * height); - png_alloc_size_t size = buffer.size(); - int success = png_image_write_to_memory(&png, buffer.data(), &size, 0, input, stride, nullptr); - if (!success && size > buffer.size()) - { - // initial buffer size guess was too small, set to the now-known size and retry - buffer.resize(size); - png.warning_or_error = 0; - success = png_image_write_to_memory(&png, buffer.data(), &size, 0, input, stride, nullptr); - } - if (!success || (png.warning_or_error & PNG_IMAGE_ERROR) != 0) - return false; + std::vector buffer; + buffer.reserve(byte_per_pixel * width * height); - File::IOFile outfile(path, "wb"); - if (!outfile) - return false; - return outfile.WriteBytes(buffer.data(), size); + std::vector rows; + rows.reserve(height); + for (u32 row = 0; row < height; row++) + { + rows.push_back(&input[row * stride]); + } + + png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); + png_infop info_ptr = png_create_info_struct(png_ptr); + + bool success = false; + if (png_ptr != nullptr && info_ptr != nullptr) + { + success = SavePNG0(png_ptr, info_ptr, png_format, width, height, level, &buffer, WriteCallback, + const_cast(rows.data())); + } + png_destroy_write_struct(&png_ptr, &info_ptr); + + if (success) + { + File::IOFile outfile(path, "wb"); + if (!outfile) + return false; + success = outfile.WriteBytes(buffer.data(), buffer.size()); + + timer.Stop(); + INFO_LOG_FMT(FRAMEDUMP, "{} byte {} by {} image saved to {} at level {} in {}", buffer.size(), + width, height, path, level, timer.GetTimeElapsedFormatted()); + } + + return success; } bool ConvertRGBAToRGBAndSavePNG(const std::string& path, const u8* input, u32 width, u32 height, - int stride) + int stride, int level) { const std::vector data = RGBAToRGB(input, width, height, stride); - return SavePNG(path, data.data(), ImageByteFormat::RGB, width, height); + return SavePNG(path, data.data(), ImageByteFormat::RGB, width, height, width * 3, level); } std::vector RGBAToRGB(const u8* input, u32 width, u32 height, int row_stride) diff --git a/Source/Core/Common/Image.h b/Source/Core/Common/Image.h index 1aa1bd4741..df6c30a3ad 100644 --- a/Source/Core/Common/Image.h +++ b/Source/Core/Common/Image.h @@ -20,9 +20,9 @@ enum class ImageByteFormat }; bool SavePNG(const std::string& path, const u8* input, ImageByteFormat format, u32 width, - u32 height, int stride = 0); + u32 height, int stride, int level = 6); bool ConvertRGBAToRGBAndSavePNG(const std::string& path, const u8* input, u32 width, u32 height, - int stride = 0); + int stride, int level); std::vector RGBAToRGB(const u8* input, u32 width, u32 height, int row_stride = 0); diff --git a/Source/Core/Common/ImageC.c b/Source/Core/Common/ImageC.c new file mode 100644 index 0000000000..daeeca899a --- /dev/null +++ b/Source/Core/Common/ImageC.c @@ -0,0 +1,27 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Common/ImageC.h" + +// Since libpng requires use of setjmp, and setjmp interacts poorly with destructors and other C++ +// features, this is in a separate C file. + +// The main purpose of this function is to allow specifying the compression level, which +// png_image_write_to_memory does not allow. row_pointers is not modified by libpng, but also isn't +// const for some reason. +bool SavePNG0(png_structp png_ptr, png_infop info_ptr, int png_format, png_uint_32 width, + png_uint_32 height, int level, png_voidp io_ptr, png_rw_ptr write_fn, + png_bytepp row_pointers) +{ + if (setjmp(png_jmpbuf(png_ptr)) != 0) + return false; + + png_set_compression_level(png_ptr, level); + png_set_IHDR(png_ptr, info_ptr, width, height, 8, png_format, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_set_rows(png_ptr, info_ptr, row_pointers); + png_set_write_fn(png_ptr, io_ptr, write_fn, NULL); + png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL); + + return true; +} diff --git a/Source/Core/Common/ImageC.h b/Source/Core/Common/ImageC.h new file mode 100644 index 0000000000..9bca859214 --- /dev/null +++ b/Source/Core/Common/ImageC.h @@ -0,0 +1,15 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" +#endif + bool + SavePNG0(png_structp png_ptr, png_infop info_ptr, int png_format, png_uint_32 width, + png_uint_32 height, int level, png_voidp io_ptr, png_rw_ptr write_fn, + png_bytepp row_pointers); diff --git a/Source/Core/Core/Config/GraphicsSettings.cpp b/Source/Core/Core/Config/GraphicsSettings.cpp index 4ce7c1b3a8..b10a20d918 100644 --- a/Source/Core/Core/Config/GraphicsSettings.cpp +++ b/Source/Core/Core/Config/GraphicsSettings.cpp @@ -49,6 +49,7 @@ const Info GFX_DUMP_PATH{{System::GFX, "Settings", "DumpPath"}, ""} const Info GFX_BITRATE_KBPS{{System::GFX, "Settings", "BitrateKbps"}, 25000}; const Info GFX_INTERNAL_RESOLUTION_FRAME_DUMPS{ {System::GFX, "Settings", "InternalResolutionFrameDumps"}, false}; +const Info GFX_PNG_COMPRESSION_LEVEL{{System::GFX, "Settings", "PNGCompressionLevel"}, 6}; const Info GFX_ENABLE_GPU_TEXTURE_DECODING{ {System::GFX, "Settings", "EnableGPUTextureDecoding"}, false}; const Info GFX_ENABLE_PIXEL_LIGHTING{{System::GFX, "Settings", "EnablePixelLighting"}, false}; diff --git a/Source/Core/Core/Config/GraphicsSettings.h b/Source/Core/Core/Config/GraphicsSettings.h index 3936b65cd9..e5289a36de 100644 --- a/Source/Core/Core/Config/GraphicsSettings.h +++ b/Source/Core/Core/Config/GraphicsSettings.h @@ -49,6 +49,7 @@ extern const Info GFX_DUMP_ENCODER; extern const Info GFX_DUMP_PATH; extern const Info GFX_BITRATE_KBPS; extern const Info GFX_INTERNAL_RESOLUTION_FRAME_DUMPS; +extern const Info GFX_PNG_COMPRESSION_LEVEL; extern const Info GFX_ENABLE_GPU_TEXTURE_DECODING; extern const Info GFX_ENABLE_PIXEL_LIGHTING; extern const Info GFX_FAST_DEPTH_CALC; diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index b547482af1..5897714436 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -110,6 +110,7 @@ + @@ -714,6 +715,11 @@ + + + NotUsing + + diff --git a/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp b/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp index 332927ea10..e604f7277a 100644 --- a/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp +++ b/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.cpp @@ -103,6 +103,7 @@ void AdvancedWidget::CreateWidgets() Config::GFX_INTERNAL_RESOLUTION_FRAME_DUMPS); m_dump_use_ffv1 = new GraphicsBool(tr("Use Lossless Codec (FFV1)"), Config::GFX_USE_FFV1); m_dump_bitrate = new GraphicsInteger(0, 1000000, Config::GFX_BITRATE_KBPS, 1000); + m_png_compression_level = new GraphicsInteger(0, 9, Config::GFX_PNG_COMPRESSION_LEVEL); dump_layout->addWidget(m_use_fullres_framedumps, 0, 0); #if defined(HAVE_FFMPEG) @@ -110,6 +111,9 @@ void AdvancedWidget::CreateWidgets() dump_layout->addWidget(new QLabel(tr("Bitrate (kbps):")), 1, 0); dump_layout->addWidget(m_dump_bitrate, 1, 1); #endif + dump_layout->addWidget(new QLabel(tr("PNG Compression Level:")), 2, 0); + m_png_compression_level->SetTitle(tr("PNG Compression Level")); + dump_layout->addWidget(m_png_compression_level, 2, 1); // Misc. auto* misc_box = new QGroupBox(tr("Misc")); @@ -251,6 +255,16 @@ void AdvancedWidget::AddDescriptions() QT_TR_NOOP("Encodes frame dumps using the FFV1 codec.

If " "unsure, leave this unchecked."); #endif + static const char TR_PNG_COMPRESSION_LEVEL_DESCRIPTION[] = + QT_TR_NOOP("Specifies the zlib compression level to use when saving PNG images (both for " + "screenshots and framedumping).

" + "Since PNG uses lossless compression, this does not affect the image quality; " + "instead, it is a trade-off between file size and compression time.

" + "A value of 0 uses no compression at all. A value of 1 uses very little " + "compression, while the maximum value of 9 applies a lot of compression. " + "However, for PNG files, levels between 3 and 6 are generally about as good as " + "level 9 but finish in significantly less time.

" + "If unsure, leave this at 6."); static const char TR_CROPPING_DESCRIPTION[] = QT_TR_NOOP( "Crops the picture from its native aspect ratio to 4:3 or " "16:9.

If unsure, leave this unchecked."); @@ -306,6 +320,7 @@ void AdvancedWidget::AddDescriptions() #ifdef HAVE_FFMPEG m_dump_use_ffv1->SetDescription(tr(TR_USE_FFV1_DESCRIPTION)); #endif + m_png_compression_level->SetDescription(tr(TR_PNG_COMPRESSION_LEVEL_DESCRIPTION)); m_enable_cropping->SetDescription(tr(TR_CROPPING_DESCRIPTION)); m_enable_prog_scan->SetDescription(tr(TR_PROGRESSIVE_SCAN_DESCRIPTION)); m_backend_multithreading->SetDescription(tr(TR_BACKEND_MULTITHREADING_DESCRIPTION)); diff --git a/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.h b/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.h index 805f1e54b3..1148a2df1b 100644 --- a/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.h +++ b/Source/Core/DolphinQt/Config/Graphics/AdvancedWidget.h @@ -52,6 +52,7 @@ private: GraphicsBool* m_dump_use_ffv1; GraphicsBool* m_use_fullres_framedumps; GraphicsInteger* m_dump_bitrate; + GraphicsInteger* m_png_compression_level; // Misc GraphicsBool* m_enable_cropping; diff --git a/Source/Core/InputCommon/ImageOperations.cpp b/Source/Core/InputCommon/ImageOperations.cpp index c7dac141c9..04293c9183 100644 --- a/Source/Core/InputCommon/ImageOperations.cpp +++ b/Source/Core/InputCommon/ImageOperations.cpp @@ -91,7 +91,7 @@ bool WriteImage(const std::string& path, const ImagePixelData& image) } return Common::SavePNG(path, buffer.data(), Common::ImageByteFormat::RGBA, image.width, - image.height); + image.height, image.width * 4); } ImagePixelData Resize(ResizeMode mode, const ImagePixelData& src, u32 new_width, u32 new_height) diff --git a/Source/Core/VideoCommon/RenderBase.cpp b/Source/Core/VideoCommon/RenderBase.cpp index 043add2996..f293977623 100644 --- a/Source/Core/VideoCommon/RenderBase.cpp +++ b/Source/Core/VideoCommon/RenderBase.cpp @@ -38,6 +38,7 @@ #include "Common/Thread.h" #include "Common/Timer.h" +#include "Core/Config/GraphicsSettings.h" #include "Core/Config/NetplaySettings.h" #include "Core/Config/SYSCONFSettings.h" #include "Core/ConfigManager.h" @@ -95,7 +96,8 @@ static float AspectToWidescreen(float aspect) static bool DumpFrameToPNG(const FrameDump::FrameData& frame, const std::string& file_name) { return Common::ConvertRGBAToRGBAndSavePNG(file_name, frame.data, frame.width, frame.height, - frame.stride); + frame.stride, + Config::Get(Config::GFX_PNG_COMPRESSION_LEVEL)); } Renderer::Renderer(int backbuffer_width, int backbuffer_height, float backbuffer_scale,