Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for setting the PNG zlib compression level #10143

Merged
merged 1 commit into from Nov 23, 2021

Conversation

Pokechu22
Copy link
Contributor

@Pokechu22 Pokechu22 commented Oct 2, 2021

When using large internal resolutions, it can take a while for the image to be compressed. This PR adds a setting to change the zlib compression level, which enables a trade-off between smaller files and faster saving. (Since PNG is lossless, the image quality is always the same).

Libpng notes this about the compression levels:

dolphin/Externals/libpng/png.h

Lines 1500 to 1509 in 890a5ed

/* Set the library compression level. Currently, valid values range from
* 0 - 9, corresponding directly to the zlib compression levels 0 - 9
* (0 - no compression, 9 - "maximal" compression). Note that tests have
* shown that zlib compression levels 3-6 usually perform as well as level 9
* for PNG images, and do considerably fewer caclulations. In the future,
* these values may not correspond directly to the zlib compression levels.
*/
#ifdef PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED
PNG_EXPORT(69, void, png_set_compression_level, (png_structrp png_ptr,
int level));

Libpng presumably uses zlib's default of 6 by default:

The compression level must be Z_DEFAULT_COMPRESSION, or between 0 and 9:
1 gives best speed, 9 gives best compression, 0 gives no compression at all
(the input data is simply copied a block at a time). Z_DEFAULT_COMPRESSION
requests a default compromise between speed and compression (currently
equivalent to level 6).

Though libpng also does have a "fast" option which uses a value of 3:

/* Apply 'fast' options if the flag is set. */
if ((image->flags & PNG_IMAGE_FLAG_FAST) != 0)
{
png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_NO_FILTERS);
/* NOTE: determined by experiment using pngstest, this reflects some
* balance between the time to write the image once and the time to read
* it about 50 times. The speed-up in pngstest was about 10-20% of the
* total (user) time on a heavily loaded system.
*/
# ifdef PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED
png_set_compression_level(png_ptr, 3);
# endif
}

Here are some results using frame 0 of the Melty Molten Galaxy fifolog with arbitrary mipmap detection enabled at auto aspect ratio and an internal resolution of 16x (producing a 12968 by 7296 image):

Level Size (bytes) Time (s) Size diff Time diff
0 284,310,113 0:02.942 100%/100% 1.0x/1.0x
1 60,348,200 0:04.437 21.2%/21% 1.5x/1.5x
2 57,772,001 0:05.307 20.3%/96% 1.8x/1.2x
3 54,902,451 0:07.170 19.3%/95% 2.4x/1.4x
4 49,811,855 0:07.917 17.5%/91% 2.7x/1.1x
5 48,492,485 0:10.111 17.1%/97% 3.4x/1.3x
6 47,381,039 0:17.710 16.7%/98% 6.0x/1.8x
7 46,835,815 0:29.212 16.5%/99% 9.9x/1.6x
8 45,792,636 1:15.136 16.1%/98% 25.5x/2.6x
9 45,346,752 2:18.179 15.9%/99% 47.0x/1.8x

The size diff and time diff values are comparisons to level 0 followed by comparisons to the previous level.

// Disable warning for setjmp in C++
#pragma warning(disable : 4611)
#endif
if (setjmp(png_jmpbuf(png_ptr)) != 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i seem to remember purposefully getting rid of setjmp usage in libpng-related code before; is it really required?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The side effects (and reason for warning) can be pretty subtle and easy to get wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code previously used the simplified libpng API (png_image_write_to_memory); as far as I can tell there is no way to set the compression level with that API.

png_image_write_to_memory calls png_safe_execute, which uses setjmp internally. So setjmp was being used before, though not directly in Dolphin code.

I did try to isolate the setjmp call to its own function and have the C++ objects be constructed and destructed outside of that function, but I'm not sure if that's guaranteed to be safe (either by the standard or in practice due to inlining).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'm the one who ported this over to the simplified API: IMO if you want to use the complex API, we should make a separate C (not C++) compilation unit and use the setjmp there. Otherwise this is inviting danger for anyone who touches the code in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a way to write it in C and have the write callback also be C that would be nice. It's not a huge deal to me either way but just a bit of unfortunate code pattern by libpng :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved SavePNG0 into a C file (which also required disabling precompiled headers for that file).

According to cppreference on longjmp,

No destructors for automatic objects are called. If replacing of std::longjmp with throw and setjmp with catch would execute a non-trivial destructor for any automatic object, the behavior of such std::longjmp is undefined.

Since longjmp isn't called by the callback, I think it's fine for the callback to be in C++ code. I also don't want to try and port the old buffer-resizing code to C, partially because I think it'd be fiddly to do and partially because if the buffer ends up being too small, I don't want to run the compression process a second time (though that probably won't happen often for higher compression levels).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I suggested that the callback be C is because std::vector::insert can throw (which comes with it's own problems but might also cause dtors to be hit on paths you wouldn't expect). But, dolphin doesn't care about such exceptions at all anyway, so 🤷‍♂️. It's fine how it is imo but just FYI

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently build dolphin with exceptions disabled, though I'm tempted to re-enable them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at least for msvc we specify /EHsc, mostly to simplify debugging if something does throw.

@leoetlino leoetlino merged commit aa5cb35 into dolphin-emu:master Nov 23, 2021
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
5 participants