diff --git a/CMakeLists.txt b/CMakeLists.txt index f21cec5bce..bdcb627693 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -762,7 +762,7 @@ macro(common_libktx_settings target enable_write library_type) ${target} PRIVATE lib/basis_encode.cpp - lib/astc_encode.cpp + lib/astc_codec.cpp ${BASISU_ENCODER_C_SRC} ${BASISU_ENCODER_CXX_SRC} lib/writer1.c diff --git a/include/ktx.h b/include/ktx.h index 2106f91b22..267d716cba 100644 --- a/include/ktx.h +++ b/include/ktx.h @@ -1312,6 +1312,9 @@ ktxTexture2_CompressAstcEx(ktxTexture2* This, ktxAstcParams* params); KTX_API KTX_error_code KTX_APIENTRY ktxTexture2_CompressAstc(ktxTexture2* This, ktx_uint32_t quality); +KTX_API KTX_error_code KTX_APIENTRY +ktxTexture2_DecodeAstc(ktxTexture2* This, ktx_uint32_t vkformat); + /** * @memberof ktxTexture2 * @~English diff --git a/lib/astc_encode.cpp b/lib/astc_codec.cpp similarity index 67% rename from lib/astc_encode.cpp rename to lib/astc_codec.cpp index a0d234fdf5..a5e48f69a7 100644 --- a/lib/astc_encode.cpp +++ b/lib/astc_codec.cpp @@ -360,7 +360,7 @@ astcSwizzle(const ktxAstcParams ¶ms) { static void astcBlockDimensions(ktx_uint32_t block_size, - uint32_t& block_x, uint32_t& block_y, uint32_t& block_z) { + uint32_t& block_x, uint32_t& block_y, uint32_t& block_z) { switch (block_size) { case KTX_PACK_ASTC_BLOCK_DIMENSION_4x4 : block_x = 4; block_y = 4; block_z = 1; break; case KTX_PACK_ASTC_BLOCK_DIMENSION_5x4 : block_x = 5; block_y = 4; block_z = 1; break; @@ -391,6 +391,88 @@ astcBlockDimensions(ktx_uint32_t block_size, } } + +static void +astcBlockDimensions(VkFormat format, + uint32_t &x, uint32_t &y, uint32_t &z) noexcept { + switch (format) { + case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: x = 4; y = 4; z = 0; break; + case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: x = 4; y = 4; z = 0; break; + case VK_FORMAT_ASTC_5x4_UNORM_BLOCK: x = 5; y = 4; z = 0; break; + case VK_FORMAT_ASTC_5x4_SRGB_BLOCK: x = 5; y = 4; z = 0; break; + case VK_FORMAT_ASTC_5x5_UNORM_BLOCK: x = 5; y = 5; z = 0; break; + case VK_FORMAT_ASTC_5x5_SRGB_BLOCK: x = 5; y = 5; z = 0; break; + case VK_FORMAT_ASTC_6x5_UNORM_BLOCK: x = 6; y = 5; z = 0; break; + case VK_FORMAT_ASTC_6x5_SRGB_BLOCK: x = 6; y = 5; z = 0; break; + case VK_FORMAT_ASTC_6x6_UNORM_BLOCK: x = 6; y = 6; z = 0; break; + case VK_FORMAT_ASTC_6x6_SRGB_BLOCK: x = 6; y = 6; z = 0; break; + case VK_FORMAT_ASTC_8x5_UNORM_BLOCK: x = 8; y = 5; z = 0; break; + case VK_FORMAT_ASTC_8x5_SRGB_BLOCK: x = 8; y = 5; z = 0; break; + case VK_FORMAT_ASTC_8x6_UNORM_BLOCK: x = 8; y = 6; z = 0; break; + case VK_FORMAT_ASTC_8x6_SRGB_BLOCK: x = 8; y = 6; z = 0; break; + case VK_FORMAT_ASTC_8x8_UNORM_BLOCK: x = 8; y = 8; z = 0; break; + case VK_FORMAT_ASTC_8x8_SRGB_BLOCK: x = 8; y = 8; z = 0; break; + case VK_FORMAT_ASTC_10x5_UNORM_BLOCK: x = 10; y = 5; z = 0; break; + case VK_FORMAT_ASTC_10x5_SRGB_BLOCK: x = 10; y = 5; z = 0; break; + case VK_FORMAT_ASTC_10x6_UNORM_BLOCK: x = 10; y = 6; z = 0; break; + case VK_FORMAT_ASTC_10x6_SRGB_BLOCK: x = 10; y = 6; z = 0; break; + case VK_FORMAT_ASTC_10x8_UNORM_BLOCK: x = 10; y = 8; z = 0; break; + case VK_FORMAT_ASTC_10x8_SRGB_BLOCK: x = 10; y = 8; z = 0; break; + case VK_FORMAT_ASTC_10x10_UNORM_BLOCK: x = 10; y = 10; z = 0; break; + case VK_FORMAT_ASTC_10x10_SRGB_BLOCK: x = 10; y = 10; z = 0; break; + case VK_FORMAT_ASTC_12x10_UNORM_BLOCK: x = 12; y = 10; z = 0; break; + case VK_FORMAT_ASTC_12x10_SRGB_BLOCK: x = 12; y = 10; z = 0; break; + case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: x = 12; y = 12; z = 0; break; + case VK_FORMAT_ASTC_12x12_SRGB_BLOCK: x = 12; y = 12; z = 0; break; + case VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK: x = 4; y = 4; z = 0; break; + case VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK: x = 5; y = 4; z = 0; break; + case VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK: x = 5; y = 5; z = 0; break; + case VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK: x = 6; y = 5; z = 0; break; + case VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK: x = 6; y = 6; z = 0; break; + case VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK: x = 8; y = 5; z = 0; break; + case VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK: x = 8; y = 6; z = 0; break; + case VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK: x = 8; y = 8; z = 0; break; + case VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK: x = 10; y = 5; z = 0; break; + case VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK: x = 10; y = 6; z = 0; break; + case VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK: x = 10; y = 8; z = 0; break; + case VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK: x = 10; y = 10; z = 0; break; + case VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK: x = 12; y = 10; z = 0; break; + case VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK: x = 12; y = 12; z = 0; break; + case VK_FORMAT_ASTC_3x3x3_UNORM_BLOCK_EXT: x = 3; y = 3; z = 3; break; + case VK_FORMAT_ASTC_3x3x3_SRGB_BLOCK_EXT: x = 3; y = 3; z = 3; break; + case VK_FORMAT_ASTC_3x3x3_SFLOAT_BLOCK_EXT: x = 3; y = 3; z = 3; break; + case VK_FORMAT_ASTC_4x3x3_UNORM_BLOCK_EXT: x = 4; y = 3; z = 3; break; + case VK_FORMAT_ASTC_4x3x3_SRGB_BLOCK_EXT: x = 4; y = 3; z = 3; break; + case VK_FORMAT_ASTC_4x3x3_SFLOAT_BLOCK_EXT: x = 4; y = 3; z = 3; break; + case VK_FORMAT_ASTC_4x4x3_UNORM_BLOCK_EXT: x = 4; y = 4; z = 3; break; + case VK_FORMAT_ASTC_4x4x3_SRGB_BLOCK_EXT: x = 4; y = 4; z = 3; break; + case VK_FORMAT_ASTC_4x4x3_SFLOAT_BLOCK_EXT: x = 4; y = 4; z = 3; break; + case VK_FORMAT_ASTC_4x4x4_UNORM_BLOCK_EXT: x = 4; y = 4; z = 4; break; + case VK_FORMAT_ASTC_4x4x4_SRGB_BLOCK_EXT: x = 4; y = 4; z = 4; break; + case VK_FORMAT_ASTC_4x4x4_SFLOAT_BLOCK_EXT: x = 4; y = 4; z = 4; break; + case VK_FORMAT_ASTC_5x4x4_UNORM_BLOCK_EXT: x = 5; y = 4; z = 4; break; + case VK_FORMAT_ASTC_5x4x4_SRGB_BLOCK_EXT: x = 5; y = 4; z = 4; break; + case VK_FORMAT_ASTC_5x4x4_SFLOAT_BLOCK_EXT: x = 5; y = 4; z = 4; break; + case VK_FORMAT_ASTC_5x5x4_UNORM_BLOCK_EXT: x = 5; y = 5; z = 4; break; + case VK_FORMAT_ASTC_5x5x4_SRGB_BLOCK_EXT: x = 5; y = 5; z = 4; break; + case VK_FORMAT_ASTC_5x5x4_SFLOAT_BLOCK_EXT: x = 5; y = 5; z = 4; break; + case VK_FORMAT_ASTC_5x5x5_UNORM_BLOCK_EXT: x = 5; y = 5; z = 5; break; + case VK_FORMAT_ASTC_5x5x5_SRGB_BLOCK_EXT: x = 5; y = 5; z = 5; break; + case VK_FORMAT_ASTC_5x5x5_SFLOAT_BLOCK_EXT: x = 5; y = 5; z = 5; break; + case VK_FORMAT_ASTC_6x5x5_UNORM_BLOCK_EXT: x = 6; y = 5; z = 5; break; + case VK_FORMAT_ASTC_6x5x5_SRGB_BLOCK_EXT: x = 6; y = 5; z = 5; break; + case VK_FORMAT_ASTC_6x5x5_SFLOAT_BLOCK_EXT: x = 6; y = 5; z = 5; break; + case VK_FORMAT_ASTC_6x6x5_UNORM_BLOCK_EXT: x = 6; y = 6; z = 5; break; + case VK_FORMAT_ASTC_6x6x5_SRGB_BLOCK_EXT: x = 6; y = 6; z = 5; break; + case VK_FORMAT_ASTC_6x6x5_SFLOAT_BLOCK_EXT: x = 6; y = 6; z = 5; break; + case VK_FORMAT_ASTC_6x6x6_UNORM_BLOCK_EXT: x = 6; y = 6; z = 6; break; + case VK_FORMAT_ASTC_6x6x6_SRGB_BLOCK_EXT: x = 6; y = 6; z = 6; break; + case VK_FORMAT_ASTC_6x6x6_SFLOAT_BLOCK_EXT: x = 6; y = 6; z = 6; break; + default: + x = 0; y = 0; z = 0; + } +} + static float astcQuality(ktx_uint32_t quality_level) { switch (quality_level) { @@ -716,8 +798,9 @@ ktxTexture2_CompressAstcEx(ktxTexture2* This, ktxAstcParams* params) { assert(KHR_DFDVAL(prototype->pDfd+1, MODEL) == KHR_DF_MODEL_ASTC && "Invalid dfd generated for ASTC image\n"); assert((transfer == KHR_DF_TRANSFER_SRGB - ? KHR_DFDVAL(prototype->pDfd+1, PRIMARIES) == KHR_DF_PRIMARIES_SRGB - : true) && "Not a valid sRGB image\n"); + ? KHR_DFDVAL(prototype->pDfd+1, TRANSFER) == KHR_DF_TRANSFER_SRGB && + KHR_DFDVAL(prototype->pDfd+1, PRIMARIES) == KHR_DF_PRIMARIES_SRGB + : true) && "Not a valid sRGB image\n"); // Fix up the current (This) texture #undef DECLARE_PRIVATE @@ -807,3 +890,232 @@ ktxTexture2_CompressAstc(ktxTexture2* This, ktx_uint32_t quality) { return ktxTexture2_CompressAstcEx(This, ¶ms); } + +struct decompression_workload +{ + astcenc_context* context; + uint8_t* data; + size_t data_len; + astcenc_image* image_out; + astcenc_swizzle swizzle; + astcenc_error error; +}; + +/** + * @brief Runner callback function for a decompression worker thread. + * + * @param thread_count The number of threads in the worker pool. + * @param thread_id The index of this thread in the worker pool. + * @param payload The parameters for this thread. + */ +static void decompression_workload_runner(int thread_count, int thread_id, void* payload) { + (void)thread_count; + + decompression_workload* work = static_cast(payload); + astcenc_error error = astcenc_decompress_image(work->context, work->data, work->data_len, + work->image_out, &work->swizzle, thread_id); + // This is a racy update, so which error gets returned is a random, but it + // will reliably report an error if an error occurs + if (error != ASTCENC_SUCCESS) + { + work->error = error; + } +} + +/** + * @brief Decodes the provided ktx2 texture if its astc encoded + * + * @param This The texture to decode + * @param vkformat The decoding format to use + */ +// will update "This" with the uncompressed copy +KTX_API KTX_error_code KTX_APIENTRY +ktxTexture2_DecodeAstc(ktxTexture2 *This, ktx_uint32_t vkformat) { + // Decompress This using astc-decoder + uint32_t* BDB = This->pDfd + 1; + khr_df_model_e colorModel = (khr_df_model_e)KHR_DFDVAL(BDB, MODEL); + if (colorModel != KHR_DF_MODEL_ASTC && This->supercompressionScheme != KTX_SS_NONE) // No supercompression supported yet + { + return KTX_INVALID_OPERATION; // Not in valid astc decodable format + } + + DECLARE_PRIVATE(priv, This); + + uint32_t channelId = KHR_DFDSVAL(BDB, 0, CHANNELID); + if (channelId == KHR_DF_CHANNEL_ASTC_DATA) { + // Found astc data + } + else + return KTX_FILE_DATA_ERROR; + + // Create a prototype texture to use for calculating sizes in the target + // format and, as useful side effects, provide us with a properly sized + // data allocation and the DFD for the target format. + ktxTextureCreateInfo createInfo; + createInfo.glInternalformat = 0; + createInfo.vkFormat = vkformat; + createInfo.baseWidth = This->baseWidth; + createInfo.baseHeight = This->baseHeight; + createInfo.baseDepth = This->baseDepth; + createInfo.generateMipmaps = This->generateMipmaps; + createInfo.isArray = This->isArray; + createInfo.numDimensions = This->numDimensions; + createInfo.numFaces = This->numFaces; + createInfo.numLayers = This->numLayers; + createInfo.numLevels = This->numLevels; + createInfo.pDfd = nullptr; + + KTX_error_code result; + ktxTexture2* prototype; + result = ktxTexture2_Create(&createInfo, KTX_TEXTURE_CREATE_ALLOC_STORAGE, &prototype); + + if (result != KTX_SUCCESS) { + assert(result == KTX_OUT_OF_MEMORY); // The only run time error + return result; + } + + if (!This->pData) { + if (ktxTexture_isActiveStream((ktxTexture*)This)) { + // Load pending. Complete it. + result = ktxTexture2_LoadImageData(This, NULL, 0); + if (result != KTX_SUCCESS) + { + ktxTexture2_Destroy(prototype); + return result; + } + } else { + // No data to transcode. + ktxTexture2_Destroy(prototype); + return KTX_INVALID_OPERATION; + } + } + + // This is where I do the decompression from "This" to prototype target + astcenc_profile profile{ASTCENC_PRF_LDR_SRGB}; + astcenc_swizzle swizzle{ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A}; + + uint32_t block_size_x{4}; // Get the right blocks from vkformat + uint32_t block_size_y{4}; + uint32_t block_size_z{1}; + float quality{ASTCENC_PRE_MEDIUM}; + uint32_t flags{0}; // TODO: Use normals mode to reconstruct normals params->normalMap ? ASTCENC_FLG_MAP_NORMAL : 0}; + + astcBlockDimensions((VkFormat)This->vkFormat, block_size_x, block_size_y, block_size_z); + + // quality = astcQuality(params->qualityLevel); + // profile = astcEncoderAction(*params, BDB); + // swizzle = astcSwizzle(*params); + + // if(params->perceptual) flags |= ASTCENC_FLG_USE_PERCEPTUAL; + + uint32_t threadCount{1}; // Decompression isn't the bottlneck and only used when checking for psnr and ssim + astcenc_config astc_config; + astcenc_context *astc_context; + astcenc_error astc_error = astcenc_config_init(profile, + block_size_x, block_size_y, block_size_z, + quality, flags, + &astc_config); + + if (astc_error != ASTCENC_SUCCESS) + return KTX_INVALID_OPERATION; + + astc_error = astcenc_context_alloc(&astc_config, threadCount, &astc_context); + + if (astc_error != ASTCENC_SUCCESS) + return KTX_INVALID_OPERATION; + + decompression_workload work; + work.context = astc_context; + work.swizzle = swizzle; + work.error = ASTCENC_SUCCESS; + + for (uint32_t levelIndex = 0; levelIndex < This->numLevels; ++levelIndex) { + const uint32_t imageWidth = std::max(This->baseWidth >> levelIndex, 1u); + const uint32_t imageHeight = std::max(This->baseHeight >> levelIndex, 1u); + const uint32_t imageDepths = std::max(This->baseDepth >> levelIndex, 1u); + + for (uint32_t layerIndex = 0; layerIndex < This->numLayers; ++layerIndex) { + for (uint32_t faceIndex = 0; faceIndex < This->numFaces; ++faceIndex) { + for (uint32_t depthSliceIndex = 0; depthSliceIndex < imageDepths; ++depthSliceIndex) { + + ktx_size_t levelImageSizeIn = ktxTexture_calcImageSize(ktxTexture(This), levelIndex, KTX_FORMAT_VERSION_TWO); + + ktx_size_t imageOffsetIn; + ktx_size_t imageOffsetOut; + + ktxTexture2_GetImageOffset(This, levelIndex, layerIndex, faceIndex + depthSliceIndex, &imageOffsetIn); + ktxTexture2_GetImageOffset(prototype, levelIndex, layerIndex, faceIndex + depthSliceIndex, &imageOffsetOut); + + auto* imageDataIn = This->pData + imageOffsetIn; + auto* imageDataOut = prototype->pData + imageOffsetOut; + + astcenc_image imageOut; + imageOut.dim_x = imageWidth; + imageOut.dim_y = imageHeight; + imageOut.dim_z = imageDepths; + imageOut.data_type = ASTCENC_TYPE_U8; // TODO: Fix for HDR types + imageOut.data = (void**)&imageDataOut; // TODO: Fix for HDR types + + work.data = imageDataIn; + work.data_len = levelImageSizeIn; + work.image_out = &imageOut; + + // Only launch worker threads for multi-threaded use - it makes basic + // single-threaded profiling and debugging a little less convoluted + if (threadCount > 1) { + launchThreads(threadCount, decompression_workload_runner, &work); + } else { + work.error = astcenc_decompress_image(work.context, work.data, work.data_len, + work.image_out, &work.swizzle, 0); + } + + // Reset ASTC context for next image + astcenc_decompress_reset(astc_context); + + if (work.error != ASTCENC_SUCCESS) { + std::cout << "ASTC decompressor failed\n" << astcenc_get_error_string(work.error) << std::endl; + + astcenc_context_free(astc_context); + return KTX_INVALID_OPERATION; + } + } + } + } + } + + // We are done with astcdecoder + astcenc_context_free(astc_context); + + if (result == KTX_SUCCESS) { + // Fix up the current texture + DECLARE_PROTECTED(thisPrtctd, This); + DECLARE_PRIVATE(protoPriv, prototype); + DECLARE_PROTECTED(protoPrtctd, prototype); + memcpy(&thisPrtctd._formatSize, &protoPrtctd._formatSize, + sizeof(ktxFormatSize)); + This->vkFormat = vkformat; + This->isCompressed = prototype->isCompressed; + This->supercompressionScheme = KTX_SS_NONE; + priv._requiredLevelAlignment = protoPriv._requiredLevelAlignment; + // Copy the levelIndex from the prototype to This. + memcpy(priv._levelIndex, protoPriv._levelIndex, + This->numLevels * sizeof(ktxLevelIndexEntry)); + // Move the DFD and data from the prototype to This. + free(This->pDfd); + This->pDfd = prototype->pDfd; + prototype->pDfd = 0; + free(This->pData); + This->pData = prototype->pData; + This->dataSize = prototype->dataSize; + prototype->pData = 0; + prototype->dataSize = 0; + // Free SGD data + This->_private->_sgdByteLength = 0; + if (This->_private->_supercompressionGlobalData) { + free(This->_private->_supercompressionGlobalData); + This->_private->_supercompressionGlobalData = NULL; + } + } + ktxTexture2_Destroy(prototype); + return result; +} diff --git a/tests/cts b/tests/cts index 96bdfcd073..4a94182783 160000 --- a/tests/cts +++ b/tests/cts @@ -1 +1 @@ -Subproject commit 96bdfcd073c9c8712eb483991da4e7fbf9e5dff4 +Subproject commit 4a941827832822fbe8df729367704f528cf65ebe diff --git a/tools/ktx/command_create.cpp b/tools/ktx/command_create.cpp index af1bfb4b5f..2d0fbaf3f6 100644 --- a/tools/ktx/command_create.cpp +++ b/tools/ktx/command_create.cpp @@ -609,8 +609,8 @@ Create a KTX2 file from various input files. is present in the input. @c SRGB or @c UNORM is chosen depending on the specified ASTC format. The ASTC-specific and common encoder options listed @ref ktx_create_options_encoding "below" become valid, otherwise they are ignored. - + This matches the functionality of the @ref ktx_encode "ktx encode" command + when an ASTC format is specified.

When used with @b \--encode it specifies the target format before the encoding step. In this case it must be one of: @@ -874,15 +874,17 @@ void CommandCreate::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& } } - const auto canCompare = options.codec == BasisCodec::BasisLZ || options.codec == BasisCodec::UASTC; + const auto basisCodec = options.codec == BasisCodec::BasisLZ || options.codec == BasisCodec::UASTC; + const auto astcCodec = isFormatAstc(options.vkFormat); + const auto canCompare = basisCodec || astcCodec; - if (canCompare) + if (basisCodec) fillOptionsCodecBasis(options); if (options.compare_ssim && !canCompare) - fatal_usage("--compare-ssim can only be used with BasisLZ or UASTC encoding."); + fatal_usage("--compare-ssim can only be used with BasisLZ, UASTC or ASTC encoding."); if (options.compare_psnr && !canCompare) - fatal_usage("--compare-psnr can only be used with BasisLZ or UASTC encoding."); + fatal_usage("--compare-psnr can only be used with BasisLZ, UASTC or ASTC encoding."); if (isFormatAstc(options.vkFormat) && !options.raw) { options.encodeASTC = true; @@ -1226,11 +1228,16 @@ void CommandCreate::encodeBasis(KTXTexture2& texture, OptionsEncodeBasis& } void CommandCreate::encodeASTC(KTXTexture2& texture, OptionsEncodeASTC& opts) { + MetricsCalculator metrics; + metrics.saveReferenceImages(texture, options, *this); + if (opts.encodeASTC) { const auto ret = ktxTexture2_CompressAstcEx(texture, &opts); if (ret != KTX_SUCCESS) fatal(rc::KTX_FAILURE, "Failed to encode KTX2 file with codec ASTC. KTX Error: {}", ktxErrorString(ret)); } + + metrics.decodeAndCalculateMetrics(texture, options, *this); } void CommandCreate::compress(KTXTexture2& texture, const OptionsDeflate& opts) { diff --git a/tools/ktx/command_encode.cpp b/tools/ktx/command_encode.cpp index 63778f5e1b..5f8d7128b5 100644 --- a/tools/ktx/command_encode.cpp +++ b/tools/ktx/command_encode.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "command.h" +#include "encode_utils_astc.h" #include "encode_utils_common.h" #include "platform_utils.h" #include "metrics_utils.h" @@ -40,12 +41,12 @@ Encode a KTX2 file. @section ktx_encode_description DESCRIPTION @b ktx @b encode can encode the KTX file specified as the @e input-file argument - to a universal format optionally supercompress the result, + to a universal format or one of the ASTC formats, optionally supercompress the result, and save it as the @e output-file. If the @e input-file is '-' the file will be read from the stdin. If the @e output-path is '-' the output file will be written to the stdout. - For universal formats, the input file must be R8, R8G8, R8G8B8 + For universal and ASTC LDR formats, the input file must be R8, R8G8, R8G8B8 or R8G8B8A8 (or their sRGB variants). @snippet{doc} ktx/deflate_utils.h command options_deflate @snippet{doc} ktx/command.h command options_generic @@ -81,7 +81,7 @@ Encode a KTX2 file. The following specific and common encoder options are available. Specific options become valid only if their encoder has been selected. Common encoder options become valid when an encoder they apply to has been selected. Otherwise they are ignored. - + @snippet{doc} ktx/encode_utils_astc.h command options_encode_astc @snippet{doc} ktx/encode_utils_basis.h command options_encode_basis @snippet{doc} ktx/encode_utils_common.h command options_encode_common @snippet{doc} ktx/metrics_utils.h command options_metrics @@ -103,11 +103,16 @@ Encode a KTX2 file. */ class CommandEncode : public Command { struct OptionsEncode { + inline static const char* kFormat = "format"; + inline static const char* kCodec = "codec"; + + VkFormat vkFormat = VK_FORMAT_UNDEFINED; + void init(cxxopts::Options& opts); void process(cxxopts::Options& opts, cxxopts::ParseResult& args, Reporter& report); }; - Combine, OptionsEncodeCommon, OptionsMetrics, OptionsDeflate, OptionsSingleInSingleOut, OptionsGeneric> options; + Combine, OptionsEncodeCommon, OptionsMetrics, OptionsDeflate, OptionsSingleInSingleOut, OptionsGeneric> options; public: virtual int main(int argc, char* argv[]) override; @@ -138,13 +143,35 @@ int CommandEncode::main(int argc, char* argv[]) { void CommandEncode::OptionsEncode::init(cxxopts::Options& opts) { opts.add_options() - ("codec", "Target codec." + (kFormat, "KTX format enum that specifies the KTX file output format." + " The enum names are matching the VkFormats without the VK_FORMAT_ prefix." + " The VK_FORMAT_ prefix is ignored if present." + "\nIt can't be used with --codec." + "\nThe value must be an ASTC format. When specified the ASTC encoder specific" + " options becomes valid." + " Case insensitive.", cxxopts::value(), "") + (kCodec, "Target codec." " With each encoding option the encoder specific options become valid," " otherwise they are ignored. Case-insensitive." "\nPossible options are: basis-lz | uastc", cxxopts::value(), ""); } -void CommandEncode::OptionsEncode::process(cxxopts::Options&, cxxopts::ParseResult&, Reporter&) { +void CommandEncode::OptionsEncode::process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) { + if (args[kCodec].count() && args[kFormat].count()) + report.fatal_usage("Format and codec can't be both specified together."); + + if (args[kFormat].count()) { + const auto formatStr = args[kFormat].as(); + const auto parsedVkFormat = parseVkFormat(formatStr); + if (!parsedVkFormat) + report.fatal_usage("The requested format is invalid or unsupported: \"{}\".", formatStr); + + vkFormat = *parsedVkFormat; + + if (!isFormatAstc(vkFormat)) { + report.fatal_usage("Optional option 'format' is not an ASTC format."); + } + } } void CommandEncode::initOptions(cxxopts::Options& opts) { @@ -156,6 +183,10 @@ void CommandEncode::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& fillOptionsCodecBasis(options); + if ((options.codec == BasisCodec::NONE || options.codec == BasisCodec::INVALID) && + options.vkFormat == VK_FORMAT_UNDEFINED) + fatal_usage("Either codec or format must be specified"); + if (options.codec == BasisCodec::BasisLZ) { if (options.zstd.has_value()) fatal_usage("Cannot encode to BasisLZ and supercompress with Zstd."); @@ -164,11 +195,17 @@ void CommandEncode::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& fatal_usage("Cannot encode to BasisLZ and supercompress with ZLIB."); } - const auto canCompare = options.codec == BasisCodec::BasisLZ || options.codec == BasisCodec::UASTC; + const auto basisCodec = options.codec == BasisCodec::BasisLZ || options.codec == BasisCodec::UASTC; + const auto astcCodec = isFormatAstc(options.vkFormat); + const auto canCompare = basisCodec || astcCodec; + if (options.compare_ssim && !canCompare) - fatal_usage("--compare-ssim can only be used with BasisLZ or UASTC encoding."); + fatal_usage("--compare-ssim can only be used with BasisLZ, UASTC or ASTC encoding."); if (options.compare_psnr && !canCompare) - fatal_usage("--compare-psnr can only be used with BasisLZ or UASTC encoding."); + fatal_usage("--compare-psnr can only be used with BasisLZ, UASTC or ASTC encoding."); + + if (astcCodec) + options.encodeASTC = true; } void CommandEncode::executeEncode() { @@ -185,6 +222,10 @@ void CommandEncode::executeEncode() { fatal(rc::INVALID_FILE, "Cannot encode KTX2 file with {} supercompression.", toString(ktxSupercmpScheme(texture->supercompressionScheme))); + const auto* bdfd = texture->pDfd + 1; + if (khr_df_model_e(KHR_DFDVAL(bdfd, MODEL)) == KHR_DF_MODEL_ASTC && options.encodeASTC) + fatal_usage("Encoding from ASTC format {} to another ASTC format {} is not supported.", toString(VkFormat(texture->vkFormat)), toString(options.vkFormat)); + switch (texture->vkFormat) { case VK_FORMAT_R8_UNORM: case VK_FORMAT_R8_SRGB: @@ -220,9 +261,18 @@ void CommandEncode::executeEncode() { MetricsCalculator metrics; metrics.saveReferenceImages(texture, options, *this); - ret = ktxTexture2_CompressBasisEx(texture, &options); - if (ret != KTX_SUCCESS) - fatal(rc::IO_FAILURE, "Failed to encode KTX2 file with codec \"{}\". KTX Error: {}", ktxErrorString(ret)); + + if (options.vkFormat != VK_FORMAT_UNDEFINED) { + options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR; // TODO: Fix me for HDR textures + ret = ktxTexture2_CompressAstcEx(texture, &options); + if (ret != KTX_SUCCESS) + fatal(rc::IO_FAILURE, "Failed to encode KTX2 file to ASTC. KTX Error: {}", ktxErrorString(ret)); + } else { + ret = ktxTexture2_CompressBasisEx(texture, &options); + if (ret != KTX_SUCCESS) + fatal(rc::IO_FAILURE, "Failed to encode KTX2 file with codec \"{}\". KTX Error: {}", options.codecName, ktxErrorString(ret)); + } + metrics.decodeAndCalculateMetrics(texture, options, *this); if (options.zstd) { diff --git a/tools/ktx/encode_utils_basis.h b/tools/ktx/encode_utils_basis.h index c6584d97d4..4817cba5a6 100644 --- a/tools/ktx/encode_utils_basis.h +++ b/tools/ktx/encode_utils_basis.h @@ -349,40 +349,26 @@ struct OptionsEncodeBasis : public ktxBasisParams { } void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) { + std::string codec_option{"encode"}; + if (ENCODE_CMD) { - // "encode" command - required "codec" argument - codec = validateBasisCodec(args["codec"]); - switch (codec) { - case BasisCodec::NONE: - report.fatal(rc::INVALID_ARGUMENTS, "Missing codec argument."); - break; - - case BasisCodec::BasisLZ: - case BasisCodec::UASTC: - codecName = to_lower_copy(args["codec"].as()); - break; - - default: - report.fatal_usage("Invalid encode codec: \"{}\".", args["codec"].as()); - break; - } - } else { - // "create" command - optional "encode" argument - codec = validateBasisCodec(args["encode"]); - switch (codec) { - case BasisCodec::NONE: - // Not specified - break; - - case BasisCodec::BasisLZ: - case BasisCodec::UASTC: - codecName = to_lower_copy(args["encode"].as()); - break; - - default: - report.fatal_usage("Invalid encode codec: \"{}\".", args["encode"].as()); - break; - } + codec_option = "codec"; + } + + codec = validateBasisCodec(args[codec_option]); + switch (codec) { + case BasisCodec::NONE: + // Not specified + break; + + case BasisCodec::BasisLZ: + case BasisCodec::UASTC: + codecName = to_lower_copy(args[codec_option].as()); + break; + + default: + report.fatal_usage("Invalid encode codec: \"{}\".", args[codec_option].as()); + break; } if (codec == BasisCodec::UASTC) { diff --git a/tools/ktx/metrics_utils.h b/tools/ktx/metrics_utils.h index 923e564b6a..7e8512fcd6 100644 --- a/tools/ktx/metrics_utils.h +++ b/tools/ktx/metrics_utils.h @@ -29,10 +29,10 @@ namespace ktx {
\--compare-ssim
Calculate encoding structural similarity index measure (SSIM) and print it to stdout. - Requires Basis-LZ or UASTC encoding.
+ Requires Basis-LZ, UASTC or ASTC encoding.
\--compare-psnr
Calculate encoding peak signal-to-noise ratio (PSNR) and print it to stdout. - Requires Basis-LZ or UASTC encoding.
+ Requires Basis-LZ, UASTC or ASTC encoding.
//! [command options_metrics] @@ -43,8 +43,8 @@ struct OptionsMetrics { void init(cxxopts::Options& opts) { opts.add_options() - ("compare-ssim", "Calculate encoding structural similarity index measure (SSIM) and print it to stdout. Requires Basis-LZ or UASTC encoding.") - ("compare-psnr", "Calculate encoding peak signal-to-noise ratio (PSNR) and print it to stdout. Requires Basis-LZ or UASTC encoding."); + ("compare-ssim", "Calculate encoding structural similarity index measure (SSIM) and print it to stdout. Requires Basis-LZ, UASTC or ASTC encoding.") + ("compare-psnr", "Calculate encoding peak signal-to-noise ratio (PSNR) and print it to stdout. Requires Basis-LZ, UASTC or ASTC encoding."); } void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter&) { @@ -101,11 +101,24 @@ class MetricsCalculator { KTXTexture2 texture{static_cast(malloc(sizeof(ktxTexture2)))}; ktxTexture2_constructCopy(texture, encodedTexture); - const auto tSwizzleInfo = determineTranscodeSwizzle(texture, report); + // Start with a default swizzle + TranscodeSwizzleInfo tSwizzleInfo{}; + tSwizzleInfo.defaultNumComponents = 4; + tSwizzleInfo.swizzle = "rgba"; ktx_error_code_e ec = KTX_SUCCESS; + // Decode the encoded texture to observe the compression losses - ec = ktxTexture2_TranscodeBasis(texture, KTX_TTF_RGBA32, 0); + const auto* bdfd = texture->pDfd + 1; + if (khr_df_model_e(KHR_DFDVAL(bdfd, MODEL)) == KHR_DF_MODEL_ASTC) + { + ec = ktxTexture2_DecodeAstc(texture, VK_FORMAT_R8G8B8A8_UNORM); + } + else + { + tSwizzleInfo = determineTranscodeSwizzle(texture, report); + ec = ktxTexture2_TranscodeBasis(texture, KTX_TTF_RGBA32, 0); + } if (ec != KTX_SUCCESS) report.fatal(rc::KTX_FAILURE, "Failed to transcode KTX2 texture to calculate error metrics: {}", ktxErrorString(ec));