From 6ed331d74c16d52aa0eef8b507f17c0c916e913c Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Wed, 26 Sep 2018 13:12:51 -0700 Subject: [PATCH] Add compression to mipgen and change bitrate to block size. The compression API now takes block size instead of bit rate. Otherwise it is quite possible for the encoder to generate a block size that is not one of the valid block sizes that graphics API's can actually consume. --- .../include/imageio/BlockCompression.h | 47 +++++++++++-- libs/imageio/src/BlockCompression.cpp | 67 +++++++++++++++---- tools/mipgen/CMakeLists.txt | 2 +- tools/mipgen/src/main.cpp | 34 +++++++++- 4 files changed, 127 insertions(+), 23 deletions(-) diff --git a/libs/imageio/include/imageio/BlockCompression.h b/libs/imageio/include/imageio/BlockCompression.h index 362c59c701..42022c589e 100644 --- a/libs/imageio/include/imageio/BlockCompression.h +++ b/libs/imageio/include/imageio/BlockCompression.h @@ -24,6 +24,8 @@ #include #include +#include + #include namespace image { @@ -44,18 +46,51 @@ enum class AstcSemantic { NORMALS, }; -// Gets passed to the encoder function to control the quality and speed of compression. -// The specified bitrate can be anywhere from 0.8 to 8.0. +// The encoder configuration controls the quality and speed of compression, as well as the resulting +// format. The specified block size must be one of the 14 block sizes that can be consumed by ES 3.2 +// as per https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glCompressedTexImage2D.xhtml struct AstcConfig { AstcPreset quality; AstcSemantic semantic; - float bitrate; + math::ushort2 blocksize; + bool srgb; +}; + +enum class AstcFormat { + RGBA_ASTC_4x4 = 0x93B0, + RGBA_ASTC_5x4 = 0x93B1, + RGBA_ASTC_5x5 = 0x93B2, + RGBA_ASTC_6x5 = 0x93B3, + RGBA_ASTC_6x6 = 0x93B4, + RGBA_ASTC_8x5 = 0x93B5, + RGBA_ASTC_8x6 = 0x93B6, + RGBA_ASTC_8x8 = 0x93B7, + RGBA_ASTC_10x5 = 0x93B8, + RGBA_ASTC_10x6 = 0x93B9, + RGBA_ASTC_10x8 = 0x93BA, + RGBA_ASTC_10x10 = 0x93BB, + RGBA_ASTC_12x10 = 0x93BC, + RGBA_ASTC_12x12 = 0x93BD, + SRGB8_ALPHA8_ASTC_4x4 = 0x93D0, + SRGB8_ALPHA8_ASTC_5x4 = 0x93D1, + SRGB8_ALPHA8_ASTC_5x5 = 0x93D2, + SRGB8_ALPHA8_ASTC_6x5 = 0x93D3, + SRGB8_ALPHA8_ASTC_6x6 = 0x93D4, + SRGB8_ALPHA8_ASTC_8x5 = 0x93D5, + SRGB8_ALPHA8_ASTC_8x6 = 0x93D6, + SRGB8_ALPHA8_ASTC_8x8 = 0x93D7, + SRGB8_ALPHA8_ASTC_10x5 = 0x93D8, + SRGB8_ALPHA8_ASTC_10x6 = 0x93D9, + SRGB8_ALPHA8_ASTC_10x8 = 0x93DA, + SRGB8_ALPHA8_ASTC_10x10 = 0x93DB, + SRGB8_ALPHA8_ASTC_12x10 = 0x93DC, + SRGB8_ALPHA8_ASTC_12x12 = 0x93DD, }; // Represents the result of compression, including which of the 28 internal formats that the // encoder finally settled on, based on the hints supplied in AstcConfig. struct AstcTexture { - const uint32_t gl_internal_format; + const AstcFormat format; const uint32_t size; std::unique_ptr data; }; @@ -66,8 +101,8 @@ AstcTexture astcCompress(const LinearImage& source, AstcConfig config); // Parses a simple underscore-delimited string to produce an ASTC compression configuration. This // makes it easy to incorporate the compression API into command-line tools. If the string is -// malformed, this returns a config with a 0 bitrate. Example strings: fast_ldr_4.2, -// thorough_normals_8.0, veryfast_hdr_1.0 +// malformed, this returns a config with a 0x0 blocksize. Example strings: fast_ldr_4x4, +// thorough_normals_6x6, veryfast_hdr_12x10 AstcConfig astcParseOptionString(const std::string& options); } // namespace image diff --git a/libs/imageio/src/BlockCompression.cpp b/libs/imageio/src/BlockCompression.cpp index 285f7e8525..00045e0960 100644 --- a/libs/imageio/src/BlockCompression.cpp +++ b/libs/imageio/src/BlockCompression.cpp @@ -40,6 +40,41 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { first = false; } + // Check the validity of the given block size. + + AstcFormat format; + if (config.blocksize == math::ushort2 {4, 4}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_4x4 : AstcFormat::RGBA_ASTC_4x4; + } else if (config.blocksize == math::ushort2 {5, 4}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_5x4 : AstcFormat::RGBA_ASTC_5x4; + } else if (config.blocksize == math::ushort2 {5, 5}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_5x5 : AstcFormat::RGBA_ASTC_5x5; + } else if (config.blocksize == math::ushort2 {6, 5}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_6x5 : AstcFormat::RGBA_ASTC_6x5; + } else if (config.blocksize == math::ushort2 {6, 6}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_6x6 : AstcFormat::RGBA_ASTC_6x6; + } else if (config.blocksize == math::ushort2 {8, 5}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_8x5 : AstcFormat::RGBA_ASTC_8x5; + } else if (config.blocksize == math::ushort2 {8, 6}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_8x6 : AstcFormat::RGBA_ASTC_8x6; + } else if (config.blocksize == math::ushort2 {8, 8}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_8x8 : AstcFormat::RGBA_ASTC_8x8; + } else if (config.blocksize == math::ushort2 {10, 5}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x5 : AstcFormat::RGBA_ASTC_10x5; + } else if (config.blocksize == math::ushort2 {10, 6}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x6 : AstcFormat::RGBA_ASTC_10x6; + } else if (config.blocksize == math::ushort2 {10, 8}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x8 : AstcFormat::RGBA_ASTC_10x8; + } else if (config.blocksize == math::ushort2 {10, 10}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x10 : AstcFormat::RGBA_ASTC_10x10; + } else if (config.blocksize == math::ushort2 {12, 10}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_12x10 : AstcFormat::RGBA_ASTC_12x10; + } else if (config.blocksize == math::ushort2 {12, 12}) { + format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_12x12 : AstcFormat::RGBA_ASTC_12x12; + } else { + return {}; + } + // Create an input image for the ARM encoder in a format that it can consume. // It expects four-channel data, so we extend or curtail the channel count in a reasonable way. // The encoder can take half-floats or bytes, but we always give it half-floats. @@ -93,13 +128,16 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { } } - // Determine the block size based on the bit rate. + // Determine the bitrate based on the specified block size. - int xdim_2d, ydim_2d; - int xdim_3d, ydim_3d, zdim_3d; - find_closest_blockdim_2d(config.bitrate, &xdim_2d, &ydim_2d, 0); - find_closest_blockdim_3d(config.bitrate, &xdim_3d, &ydim_3d, &zdim_3d, 0); + int xdim_2d = config.blocksize.x, ydim_2d = config.blocksize.y; const float log10_texels_2d = std::log((float)(xdim_2d * ydim_2d)) / std::log(10.0f); + const float bitrate = 128.0 / (xdim_2d * ydim_2d); + + // We do not fully support 3D textures yet, but we include some of the 3D config params anyway. + + int xdim_3d, ydim_3d, zdim_3d; + find_closest_blockdim_3d(bitrate, &xdim_3d, &ydim_3d, &zdim_3d, 0); const float log10_texels_3d = std::log((float)(xdim_3d * ydim_3d * zdim_3d)) / log(10.0f); // Set up presets. @@ -173,11 +211,6 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { int xdim = xdim_2d, ydim = ydim_2d, zdim = 1; expand_block_artifact_suppression(xdim, ydim, zdim, &ewp); - // To help with debugging, dump the encoding settings in a format similar to the astcenc tool - printf("2D Block size: %dx%d (%.2f bpp)\n", xdim_2d, ydim_2d, 128.0 / (xdim_2d * ydim_2d)); - printf("3D Block size: %dx%dx%d (%.2f bpp)\n", - xdim_3d, ydim_3d, zdim_3d, 128.0 / (xdim_3d * ydim_3d * zdim_3d)); - // Perform compression. constexpr int threadcount = 1; // TODO: set this thread count @@ -200,8 +233,8 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { destroy_image(input_image); - return AstcTexture { - .gl_internal_format = 0, // TODO: figure out the correct GL enum here + return { + .format = format, .size = size, .data = decltype(AstcTexture::data)(buffer) }; @@ -215,7 +248,7 @@ AstcConfig astcParseOptionString(const string& configString) { } string quality = configString.substr(0, _1); string semantic = configString.substr(_1 + 1, _2 - _1 - 1); - string bitrate = configString.substr(_2 + 1); + string blocksize = configString.substr(_2 + 1); AstcConfig config; if (quality == "veryfast") { config.quality = AstcPreset::VERYFAST; @@ -239,7 +272,13 @@ AstcConfig astcParseOptionString(const string& configString) { } else { return {}; } - config.bitrate = std::stof(bitrate); + + const size_t _x = blocksize.find('x'); + if (_x == string::npos) { + return {}; + } + config.blocksize[0] = std::stoi(blocksize.substr(0, _x)); + config.blocksize[1] = std::stoi(blocksize.substr(_x + 1)); return config; } diff --git a/tools/mipgen/CMakeLists.txt b/tools/mipgen/CMakeLists.txt index a15b17227c..5633f49fd1 100644 --- a/tools/mipgen/CMakeLists.txt +++ b/tools/mipgen/CMakeLists.txt @@ -12,7 +12,7 @@ set(SRCS src/main.cpp) # Target definitions # ================================================================================================== add_executable(${TARGET} ${SRCS}) -target_link_libraries(${TARGET} PRIVATE math utils z image imageio getopt) +target_link_libraries(${TARGET} PRIVATE math utils z image imageio getopt stb) # ================================================================================================= # Licenses diff --git a/tools/mipgen/src/main.cpp b/tools/mipgen/src/main.cpp index 66f06f3db2..f7604a4300 100644 --- a/tools/mipgen/src/main.cpp +++ b/tools/mipgen/src/main.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -72,18 +73,21 @@ Options: specify output file format, inferred from output pattern if omitted --kernel=[box|nearest|hermite|gaussian|normals|mitchell|lanczos|min], -k [filter] specify filter kernel type (defaults to lanczos) + the "normals" filter may automatically change the compression scheme --strip-alpha ignore the alpha component of the input image --compression=COMPRESSION, -c COMPRESSION format specific compression: + KTX: astc_[fast/thorough]_[ldr/hdr]_WxH, where WxH is a valid block size PNG: Ignored Radiance: Ignored Photoshop: 16 (default), 32 OpenEXR: RAW, RLE, ZIPS, ZIP, PIZ (default) DDS: 8, 16 (default), 32 -Example: +Examples: MIPGEN -g --kernel=hermite grassland.png mip_%03d.png + MIPGEN -f ktx --compression=astc_fast_ldr_4x4 grassland.png mips.ktx )TXT"; static const char* HTML_PREFIX = R"HTML( @@ -271,9 +275,35 @@ int main(int argc, char* argv[]) { info.glInternalFormat = info.glBaseInternalFormat = KtxBundle::LUMINANCE; } + AstcConfig astcConfig {}; + if (!g_compression.empty()) { + if (g_compression.substr(0, 5) == "astc_") { + string suffix = g_compression.substr(5); + astcConfig = astcParseOptionString(suffix); + auto block = astcConfig.blocksize; + printf("Compressing with blocksize = %dx%d\n", block[0], block[1]); + } + if (astcConfig.blocksize[0] == 0) { + cerr << "Unrecognized compression: " << g_compression << endl; + return 1; + } + } uint32_t mip = 0; - auto addLevel = [&container, &mip](const LinearImage& image) { + auto addLevel = [&container, &mip, astcConfig, inputPath](const LinearImage& image) { std::unique_ptr data; + if (astcConfig.blocksize[0] > 0) { + // The ASTC encoder calls exit(1) if it fails, so it's very useful to print some + // source image information here for when this is invoked from a build script. + // Note that the encoder has limitations in terms of image size. + printf("Starting ASTC compression for %s (%dx%d)\n", inputPath.getName().c_str(), + image.getWidth(), image.getHeight()); + AstcTexture tex = astcCompress(image, astcConfig); + // Add newline here because the ASTC encoder has a progress indicator that issues a + // carriage return without a line feed. + putc('\n', stdout); + container.setBlob({mip++}, tex.data.get(), tex.size); + return; + } if (g_grayscale && g_linearized) { data = fromLinearToGrayscale(image); } else if (g_grayscale) {