/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #define STB_DXT_IMPLEMENTATION #include namespace image { static LinearImage extendToFourChannels(LinearImage source); CompressedTexture astcCompress(const LinearImage& original, AstcConfig config) { // If this is the first time, initialize the ARM encoder tables. static bool first = true; if (first) { test_inappropriate_extended_precision(); prepare_angular_tables(); build_quantization_mode_table(); first = false; } // Check the validity of the given block size. using Format = CompressedFormat; Format format; if (config.blocksize == math::ushort2 {4, 4}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_4x4 : Format::RGBA_ASTC_4x4; } else if (config.blocksize == math::ushort2 {5, 4}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_5x4 : Format::RGBA_ASTC_5x4; } else if (config.blocksize == math::ushort2 {5, 5}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_5x5 : Format::RGBA_ASTC_5x5; } else if (config.blocksize == math::ushort2 {6, 5}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_6x5 : Format::RGBA_ASTC_6x5; } else if (config.blocksize == math::ushort2 {6, 6}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_6x6 : Format::RGBA_ASTC_6x6; } else if (config.blocksize == math::ushort2 {8, 5}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_8x5 : Format::RGBA_ASTC_8x5; } else if (config.blocksize == math::ushort2 {8, 6}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_8x6 : Format::RGBA_ASTC_8x6; } else if (config.blocksize == math::ushort2 {8, 8}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_8x8 : Format::RGBA_ASTC_8x8; } else if (config.blocksize == math::ushort2 {10, 5}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x5 : Format::RGBA_ASTC_10x5; } else if (config.blocksize == math::ushort2 {10, 6}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x6 : Format::RGBA_ASTC_10x6; } else if (config.blocksize == math::ushort2 {10, 8}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x8 : Format::RGBA_ASTC_10x8; } else if (config.blocksize == math::ushort2 {10, 10}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x10 : Format::RGBA_ASTC_10x10; } else if (config.blocksize == math::ushort2 {12, 10}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_12x10 : Format::RGBA_ASTC_12x10; } else if (config.blocksize == math::ushort2 {12, 12}) { format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_12x12 : Format::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. LinearImage source = extendToFourChannels(original); const uint32_t width = source.getWidth(); const uint32_t height = source.getHeight(); astc_codec_image* input_image = allocate_image(16, width, height, 1, 0); for (int y = 0; y < height; y++) { auto imagedata16 = input_image->imagedata16[0][y]; float const* src = source.getPixelRef(0, y); for (int x = 0; x < width; x++) { imagedata16[4 * x] = float_to_sf16(src[4 * x], SF_NEARESTEVEN); imagedata16[4 * x + 1] = float_to_sf16(src[4 * x + 1], SF_NEARESTEVEN); imagedata16[4 * x + 2] = float_to_sf16(src[4 * x + 2], SF_NEARESTEVEN); imagedata16[4 * x + 3] = float_to_sf16(src[4 * x + 3], SF_NEARESTEVEN); } } // Determine the bitrate based on the specified block size. 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. int plimit_autoset; float oplimit_autoset; float dblimit_autoset_2d; float dblimit_autoset_3d; float bmc_autoset; float mincorrel_autoset; int maxiters_autoset; int pcdiv; switch (config.quality) { case AstcPreset::VERYFAST: plimit_autoset = 2; oplimit_autoset = 1.0; dblimit_autoset_2d = fmax(70 - 35 * log10_texels_2d, 53 - 19 * log10_texels_2d); dblimit_autoset_3d = fmax(70 - 35 * log10_texels_3d, 53 - 19 * log10_texels_3d); bmc_autoset = 25; mincorrel_autoset = 0.5; maxiters_autoset = 1; switch (ydim_2d) { case 4: pcdiv = 240; break; case 5: pcdiv = 56; break; case 6: pcdiv = 64; break; case 8: pcdiv = 47; break; case 10: pcdiv = 36; break; case 12: pcdiv = 30; break; default: pcdiv = 30; break; } break; case AstcPreset::FAST: plimit_autoset = 4; oplimit_autoset = 1.0; dblimit_autoset_2d = fmax(85 - 35 * log10_texels_2d, 63 - 19 * log10_texels_2d); dblimit_autoset_3d = fmax(85 - 35 * log10_texels_3d, 63 - 19 * log10_texels_3d); bmc_autoset = 50; mincorrel_autoset = 0.5; maxiters_autoset = 1; switch (ydim_2d) { case 4: pcdiv = 60; break; case 5: pcdiv = 27; break; case 6: pcdiv = 30; break; case 8: pcdiv = 24; break; case 10: pcdiv = 16; break; case 12: pcdiv = 20; break; default: pcdiv = 20; break; } break; case AstcPreset::MEDIUM: plimit_autoset = 25; oplimit_autoset = 1.2; dblimit_autoset_2d = fmax(95 - 35 * log10_texels_2d, 70 - 19 * log10_texels_2d); dblimit_autoset_3d = fmax(95 - 35 * log10_texels_3d, 70 - 19 * log10_texels_3d); bmc_autoset = 75; mincorrel_autoset = 0.75; maxiters_autoset = 2; switch (ydim_2d) { case 4: pcdiv = 25; break; case 5: pcdiv = 15; break; case 6: pcdiv = 15; break; case 8: pcdiv = 10; break; case 10: pcdiv = 8; break; case 12: pcdiv = 6; break; default: pcdiv = 6; break; } break; case AstcPreset::THOROUGH: plimit_autoset = 100; oplimit_autoset = 2.5; dblimit_autoset_2d = fmax(105 - 35 * log10_texels_2d, 77 - 19 * log10_texels_2d); dblimit_autoset_3d = fmax(105 - 35 * log10_texels_3d, 77 - 19 * log10_texels_3d); bmc_autoset = 95; mincorrel_autoset = 0.95f; maxiters_autoset = 4; switch (ydim_2d) { case 4: pcdiv = 12; break; case 5: pcdiv = 7; break; case 6: pcdiv = 7; break; case 8: pcdiv = 5; break; case 10: pcdiv = 4; break; case 12: pcdiv = 3; break; default: pcdiv = 3; break; } break; case AstcPreset::EXHAUSTIVE: plimit_autoset = 1 << 10; oplimit_autoset = 1000.0; dblimit_autoset_2d = 999.0f; dblimit_autoset_3d = 999.0f; bmc_autoset = 100; mincorrel_autoset = 0.99; maxiters_autoset = 4; switch (ydim_2d) { case 4: pcdiv = 3; break; case 5: pcdiv = 1; break; case 6: pcdiv = 1; break; case 8: pcdiv = 1; break; case 10: pcdiv = 1; break; case 12: pcdiv = 1; break; default: pcdiv = 1; break; } break; } if (plimit_autoset < 1) { plimit_autoset = 1; } else if (plimit_autoset > PARTITION_COUNT) { plimit_autoset = PARTITION_COUNT; } error_weighting_params ewp; ewp.rgb_power = 1.0f; ewp.alpha_power = 1.0f; ewp.rgb_base_weight = 1.0f; ewp.alpha_base_weight = 1.0f; ewp.rgb_mean_weight = 0.0f; ewp.rgb_stdev_weight = 0.0f; ewp.alpha_mean_weight = 0.0f; ewp.alpha_stdev_weight = 0.0f; ewp.rgb_mean_and_stdev_mixing = 0.0f; ewp.mean_stdev_radius = 0; ewp.enable_rgb_scale_with_alpha = 0; ewp.alpha_radius = 0; ewp.block_artifact_suppression = 0.0f; ewp.rgba_weights[0] = 1.0f; ewp.rgba_weights[1] = 1.0f; ewp.rgba_weights[2] = 1.0f; ewp.rgba_weights[3] = 1.0f; ewp.ra_normal_angular_scale = 0; ewp.max_refinement_iters = maxiters_autoset; ewp.block_mode_cutoff = bmc_autoset / 100.0f; ewp.texel_avg_error_limit = pow(0.1f, dblimit_autoset_2d * 0.1f) * 65535.0f * 65535.0f; ewp.partition_1_to_2_limit = oplimit_autoset; ewp.lowest_correlation_cutoff = mincorrel_autoset; ewp.partition_search_limit = plimit_autoset; // For now we do not support 3D textures but we keep the variable names consistent // with what's found in the ARM standalone tool. int xdim = xdim_2d, ydim = ydim_2d, zdim = 1; expand_block_artifact_suppression(xdim, ydim, zdim, &ewp); // Perform compression. swizzlepattern swz_encode = { 0, 1, 2, 3 }; swizzlepattern swz_decode = { 0, 1, 2, 3 }; astc_decode_mode decode_mode; switch (config.semantic) { case AstcSemantic::COLORS_LDR: decode_mode = config.srgb ? DECODE_LDR_SRGB : DECODE_LDR; break; case AstcSemantic::COLORS_HDR: decode_mode = DECODE_HDR; break; case AstcSemantic::NORMALS: decode_mode = config.srgb ? DECODE_LDR_SRGB : DECODE_LDR; ewp.rgba_weights[0] = 1.0f; ewp.rgba_weights[1] = 0.0f; ewp.rgba_weights[2] = 0.0f; ewp.rgba_weights[3] = 1.0f; ewp.ra_normal_angular_scale = 1; swz_encode.r = 0; swz_encode.g = 0; swz_encode.b = 0; swz_encode.a = 1; swz_decode.r = 0; swz_decode.g = 3; swz_decode.b = 6; swz_decode.a = 5; ewp.block_artifact_suppression = 1.8f; ewp.mean_stdev_radius = 3; ewp.rgb_mean_weight = 0; ewp.rgb_stdev_weight = 50; ewp.rgb_mean_and_stdev_mixing = 0.0; ewp.alpha_mean_weight = 0; ewp.alpha_stdev_weight = 50; break; } const int threadcount = std::thread::hardware_concurrency(); const int xsize = input_image->xsize; const int ysize = input_image->ysize; const int zsize = input_image->zsize; const int xblocks = (xsize + xdim - 1) / xdim; const int yblocks = (ysize + ydim - 1) / ydim; const int zblocks = (zsize + zdim - 1) / zdim; uint32_t size = xblocks * yblocks * zblocks * 16; uint8_t* buffer = new uint8_t[size]; encode_astc_image(input_image, nullptr, xdim, ydim, zdim, &ewp, decode_mode, swz_encode, swz_decode, buffer, 0, threadcount); destroy_image(input_image); return { .format = format, .size = size, .data = decltype(CompressedTexture::data)(buffer) }; } AstcConfig astcParseOptionString(const std::string& configString) { const size_t _1 = configString.find('_'); const size_t _2 = configString.find('_', _1 + 1); if (_1 == std::string::npos || _2 == std::string::npos) { return {}; } std::string quality = configString.substr(0, _1); std::string semantic = configString.substr(_1 + 1, _2 - _1 - 1); std::string blocksize = configString.substr(_2 + 1); AstcConfig config; if (quality == "veryfast") { config.quality = AstcPreset::VERYFAST; } else if (quality == "fast") { config.quality = AstcPreset::FAST; } else if (quality == "medium") { config.quality = AstcPreset::MEDIUM; } else if (quality == "thorough") { config.quality = AstcPreset::THOROUGH; } else if (quality == "exhaustive") { config.quality = AstcPreset::EXHAUSTIVE; } else { return {}; } if (semantic == "ldr") { config.semantic = AstcSemantic::COLORS_LDR; } else if (semantic == "hdr") { config.semantic = AstcSemantic::COLORS_HDR; } else if (semantic == "normals") { config.semantic = AstcSemantic::NORMALS; } else { return {}; } const size_t _x = blocksize.find('x'); if (_x == std::string::npos) { return {}; } config.blocksize[0] = std::stoi(blocksize.substr(0, _x)); config.blocksize[1] = std::stoi(blocksize.substr(_x + 1)); return config; } static uint32_t imin(uint32_t a, uint32_t b) { return (a < b) ? a : b; } static void extract4x4RGBA(uint8_t* dst, const LinearImage& source, uint32_t x0, uint32_t y0) { const uint32_t maxx = source.getWidth() - 1; const uint32_t maxy = source.getHeight() - 1; for (uint32_t y = y0, y1 = y0 + 4; y < y1; ++y) { for (uint32_t x = x0, x1 = x0 + 4; x < x1; ++x, dst += 4) { int clamped_x = imin(maxx, x); int clamped_y = imin(maxy, y); float const* rgba = source.getPixelRef(clamped_x, clamped_y); dst[0] = (uint8_t) (rgba[0] * 255.0f); dst[1] = (uint8_t) (rgba[1] * 255.0f); dst[2] = (uint8_t) (rgba[2] * 255.0f); dst[3] = (uint8_t) (rgba[3] * 255.0f); } } } // Our S3TC / DXT encoder uses the STB implementation by Fabian Giesen. // // Due to limitations in STB, this only supports the following formats: // - DXT1 with no alpha (16 input pixels in 64 bits of output, 6:1) // - DXT5 with alpha (16 input pixels into 128 bits of output, 4:1) // // TODO: investigate using something more capable than STB (eg AMD Compressenator, bimg, libsquish) CompressedTexture s3tcCompress(const LinearImage& original, S3tcConfig config) { const bool dxt5 = config.format == CompressedFormat::RGBA_S3TC_DXT5; LinearImage source = extendToFourChannels(original); uint8_t block[64]; uint32_t xblocks = (source.getWidth() + 3) / 4; uint32_t yblocks = (source.getHeight() + 3) / 4; uint32_t size = xblocks * yblocks * (dxt5 ? 16 : 8); uint8_t* buffer = new uint8_t[size]; uint8_t* dst = buffer; for (int y = 0, h = source.getHeight(); y < h; y += 4) { for (int x = 0, w = source.getWidth(); x < w; x += 4) { extract4x4RGBA(block, source, x, y); stb_compress_dxt_block(dst, block, dxt5, 8); dst += dxt5 ? 16 : 8; } } return { .format = config.format, .size = size, .data = decltype(CompressedTexture::data)(buffer) }; } S3tcConfig s3tcParseOptionString(const std::string& options) { if (options == "rgb_dxt1") { return {CompressedFormat::RGB_S3TC_DXT1, false}; } if (options == "rgba_dxt5") { return {CompressedFormat::RGBA_S3TC_DXT5, false}; } return {}; } CompressedTexture etcCompress(const LinearImage& original, EtcConfig config) { LinearImage source = extendToFourChannels(original); const int threadcount = std::thread::hardware_concurrency(); Etc::Image::Format etcformat; switch (config.format) { case CompressedFormat::R11_EAC: etcformat = Etc::Image::Format::R11; break; case CompressedFormat::SIGNED_R11_EAC: etcformat = Etc::Image::Format::SIGNED_R11; break; case CompressedFormat::RG11_EAC: etcformat = Etc::Image::Format::RG11; break; case CompressedFormat::SIGNED_RG11_EAC: etcformat = Etc::Image::Format::SIGNED_RG11; break; case CompressedFormat::RGB8_ETC2: etcformat = Etc::Image::Format::RGB8; break; case CompressedFormat::SRGB8_ETC2: etcformat = Etc::Image::Format::SRGB8; break; case CompressedFormat::RGB8_ALPHA1_ETC2: etcformat = Etc::Image::Format::RGB8A1; break; case CompressedFormat::SRGB8_ALPHA1_ETC: etcformat = Etc::Image::Format::SRGB8A1; break; case CompressedFormat::RGBA8_ETC2_EAC: etcformat = Etc::Image::Format::RGBA8; break; case CompressedFormat::SRGB8_ALPHA8_ETC2_EAC: etcformat = Etc::Image::Format::SRGBA8; break; default: return {}; } Etc::ErrorMetric etcmetric; switch (config.metric) { case EtcErrorMetric::RGBA: etcmetric = Etc::RGBA; break; case EtcErrorMetric::RGBX: etcmetric = Etc::RGBX; break; case EtcErrorMetric::REC709: etcmetric = Etc::REC709; break; case EtcErrorMetric::NUMERIC: etcmetric = Etc::NUMERIC; break; case EtcErrorMetric::NORMALXYZ: etcmetric = Etc::NORMALXYZ; break; default: return {}; } unsigned char *paucEncodingBits; unsigned int uiEncodingBitsBytes; unsigned int uiExtendedWidth; unsigned int uiExtendedHeight; int iEncodingTime_ms; // The etc2comp API doesn't tell you that you need to free paucEncodingBits, but they have a // commented-out "delete[] m_paucEncodingBits" in their Image destructor, which is essentially // what our unique_ptr wrapper does (CompressedTexture::data). Etc::Encode(source.getPixelRef(0, 0), source.getWidth(), source.getHeight(), etcformat, etcmetric, config.effort, threadcount, 1024, &paucEncodingBits, &uiEncodingBitsBytes, &uiExtendedWidth, &uiExtendedHeight, &iEncodingTime_ms); return { .format = config.format, .size = uiEncodingBitsBytes, .data = decltype(CompressedTexture::data)(paucEncodingBits) }; } EtcConfig etcParseOptionString(const std::string& options) { EtcConfig result {}; const size_t _2 = options.rfind('_'); const size_t _1 = options.rfind('_', _2 - 1); std::string sformat = options.substr(0, _1); std::string smetric = options.substr(_1 + 1, _2 - _1 - 1); std::string seffort = options.substr(_2 + 1); if (sformat == "r11") { result.format = CompressedFormat::R11_EAC; } else if (sformat == "signed_r11") { result.format = CompressedFormat::SIGNED_R11_EAC; } else if (sformat == "rg11") { result.format = CompressedFormat::RG11_EAC; } else if (sformat == "signed_rg11") { result.format = CompressedFormat::SIGNED_RG11_EAC; } else if (sformat == "rgb8") { result.format = CompressedFormat::RGB8_ETC2; } else if (sformat == "srgb8") { result.format = CompressedFormat::SRGB8_ETC2; } else if (sformat == "rgb8_alpha") { result.format = CompressedFormat::RGB8_ALPHA1_ETC2; } else if (sformat == "srgb8_alpha") { result.format = CompressedFormat::SRGB8_ALPHA1_ETC; } else if (sformat == "rgba8") { result.format = CompressedFormat::RGBA8_ETC2_EAC; } else if (sformat == "srgb8_alpha8") { result.format = CompressedFormat::SRGB8_ALPHA8_ETC2_EAC; } if (smetric == "rgba") { result.metric = EtcErrorMetric::RGBA; } else if (smetric == "rgbx") { result.metric = EtcErrorMetric::RGBX; } else if (smetric == "rec709") { result.metric = EtcErrorMetric::REC709; } else if (smetric == "numeric") { result.metric = EtcErrorMetric::NUMERIC; } else if (smetric == "normalxyz") { result.metric = EtcErrorMetric::NORMALXYZ; } result.effort = std::stoi(seffort); return result; } bool parseOptionString(const std::string& options, CompressionConfig* config) { config->type = CompressionConfig::INVALID; if (options.substr(0, 5) == "astc_") { config->astc = astcParseOptionString(options.substr(5)); if (config->astc.blocksize[0] != 0) { config->type = CompressionConfig::ASTC; } } else if (options.substr(0, 5) == "s3tc_") { config->s3tc = s3tcParseOptionString(options.substr(5)); if (config->s3tc.format != CompressedFormat::INVALID) { config->type = CompressionConfig::S3TC; } } else if (options.substr(0, 4) == "etc_") { config->etc = etcParseOptionString(options.substr(4)); if (config->etc.format != CompressedFormat::INVALID) { config->type = CompressionConfig::ETC; } } return config->type != CompressionConfig::INVALID; } CompressedTexture compressTexture(const CompressionConfig& config, const LinearImage& image) { if (config.type == CompressionConfig::ASTC) { return astcCompress(image, config.astc); } if (config.type == CompressionConfig::S3TC) { return s3tcCompress(image, config.s3tc); } if (config.type == CompressionConfig::ETC) { return etcCompress(image, config.etc); } return {}; } static LinearImage extendToFourChannels(LinearImage original) { LinearImage source = original; const uint32_t width = source.getWidth(); const uint32_t height = source.getHeight(); auto createEmptyImage = [width, height](float value) { auto result = LinearImage(width, height, 1); float* pixels = result.getPixelRef(0, 0); std::fill(pixels, pixels + width * height, value); return result; }; switch (source.getChannels()) { case 4: break; case 1: { auto l = image::extractChannel(source, 0); auto a = createEmptyImage(1.0f); source = image::combineChannels({l, l, l, a}); } case 2: { auto l = image::extractChannel(source, 0); auto a = image::extractChannel(source, 1); source = image::combineChannels({l, l, l, a}); } case 3: { auto r = image::extractChannel(source, 0); auto g = image::extractChannel(source, 1); auto b = image::extractChannel(source, 2); auto a = createEmptyImage(1.0f); source = image::combineChannels({r, g, b, a}); } default: { auto r = image::extractChannel(source, 0); auto g = image::extractChannel(source, 1); auto b = image::extractChannel(source, 2); auto a = image::extractChannel(source, 3); source = image::combineChannels({r, g, b, a}); } } return source; } } // namespace image