// Copyright 2022-2023 The Khronos Group Inc. // Copyright 2022-2023 RasterGrid Kft. // 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" #include "deflate_utils.h" #include "encode_utils_basis.h" #include "formats.h" #include "sbufstream.h" #include "utility.h" #include "validate.h" #include "ktx.h" #include #include #include #include #include #include #include #include // ------------------------------------------------------------------------------------------------- namespace ktx { // ------------------------------------------------------------------------------------------------- /** @page ktx_encode ktx encode @~English Encode a KTX2 file. @section ktx_encode_synopsis SYNOPSIS ktx encode [option...] @e input-file @e output-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 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 and ASTC LDR formats, the input file must be R8, R8G8, R8G8B8 or R8G8B8A8 (or their sRGB variants). If the input file is invalid the first encountered validation error is displayed to the stderr and the command exits with the relevant non-zero status code. @section ktx\_encode\_options OPTIONS @subsection ktx\_encode\_options\_general General Options The following options are available:
\--codec basis-lz | uastc
Target codec followed by the codec specific options. With each choice the specific and common encoder options listed @ref ktx\_encode\_options\_encoding "below" become valid, otherwise they are ignored. Case-insensitive.
@snippet{doc} ktx/encode_utils_basis.h command options_basis_encoders
\--format
KTX format enum that specifies the target ASTC format. Non-ASTC formats are invalid. When specified the ASTC-specific and common encoder options listed @ref ktx\_encode\_options\_encoding "below" become valid, otherwise they are ignored.
@snippet{doc} ktx/deflate_utils.h command options_deflate @snippet{doc} ktx/command.h command options_generic @subsection ktx\_encode\_options\_encoding Specific and Common Encoding Options 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 @section ktx_encode_exitstatus EXIT STATUS @snippet{doc} ktx/command.h command exitstatus @section ktx_encode_history HISTORY @par Version 4.0 - Initial version. @par Version 4.4 - Reorganize encoding options. @section ktx_encode_author AUTHOR - Mátyás Császár [Vader], RasterGrid www.rastergrid.com - Daniel Rákos, RasterGrid www.rastergrid.com */ 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; public: virtual int main(int argc, char* argv[]) override; virtual void initOptions(cxxopts::Options& opts) override; virtual void processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) override; private: void executeEncode(); }; // ------------------------------------------------------------------------------------------------- int CommandEncode::main(int argc, char* argv[]) { try { parseCommandLine("ktx encode", "Encode the KTX file specified as the input-file argument,\n" " optionally supercompress the result, and save it as the output-file.", argc, argv); executeEncode(); return +rc::SUCCESS; } catch (const FatalError& error) { return +error.returnCode; } catch (const std::exception& e) { fmt::print(std::cerr, "{} fatal: {}\n", commandName, e.what()); return +rc::RUNTIME_ERROR; } } void CommandEncode::OptionsEncode::init(cxxopts::Options& opts) { opts.add_options() (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& 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) { options.init(opts); } void CommandEncode::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) { options.process(opts, args, *this); 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."); if (options.zlib.has_value()) fatal_usage("Cannot encode to BasisLZ and supercompress with ZLIB."); } 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, UASTC or ASTC encoding."); if (options.compare_psnr && !canCompare) fatal_usage("--compare-psnr can only be used with BasisLZ, UASTC or ASTC encoding."); if (astcCodec) options.encodeASTC = true; } void CommandEncode::executeEncode() { InputStream inputStream(options.inputFilepath, *this); validateToolInput(inputStream, fmtInFile(options.inputFilepath), *this); KTXTexture2 texture{nullptr}; StreambufStream ktx2Stream{inputStream->rdbuf(), std::ios::in | std::ios::binary}; auto ret = ktxTexture2_CreateFromStream(ktx2Stream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, texture.pHandle()); if (ret != KTX_SUCCESS) fatal(rc::INVALID_FILE, "Failed to create KTX2 texture: {}", ktxErrorString(ret)); if (texture->supercompressionScheme != KTX_SS_NONE) 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: case VK_FORMAT_R8G8_UNORM: case VK_FORMAT_R8G8_SRGB: case VK_FORMAT_R8G8B8_UNORM: case VK_FORMAT_R8G8B8_SRGB: case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: // Allowed formats break; default: fatal_usage("Only R8, RG8, RGB8, or RGBA8 UNORM and SRGB formats can be encoded, " "but format is {}.", toString(VkFormat(texture->vkFormat))); break; } // Convert 1D textures to 2D (we could consider 1D as an invalid input) texture->numDimensions = std::max(2u, texture->numDimensions); // Modify KTXwriter metadata const auto writer = fmt::format("{} {}", commandName, version(options.testrun)); ktxHashList_DeleteKVPair(&texture->kvDataHead, KTX_WRITER_KEY); ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY, static_cast(writer.size() + 1), // +1 to include the \0 writer.c_str()); khr_df_transfer_e tf = ktxTexture2_GetTransferFunction_e(texture); if (options.ktxBasisParams::normalMap && tf != KHR_DF_TRANSFER_LINEAR) fatal(rc::INVALID_FILE, "--normal-mode specified but the input file uses non-linear transfer function {}.", toString(tf)); MetricsCalculator metrics; metrics.saveReferenceImages(texture, options, *this); 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) { ret = ktxTexture2_DeflateZstd(texture, *options.zstd); if (ret != KTX_SUCCESS) fatal(rc::IO_FAILURE, "Zstd deflation failed. KTX Error: {}", ktxErrorString(ret)); } if (options.zlib) { ret = ktxTexture2_DeflateZLIB(texture, *options.zlib); if (ret != KTX_SUCCESS) fatal(rc::IO_FAILURE, "ZLIB deflation failed. KTX Error: {}", ktxErrorString(ret)); } // Add KTXwriterScParams metadata const auto writerScParams = fmt::format("{}{}{}", options.codecOptions, options.commonOptions, options.compressOptions); ktxHashList_DeleteKVPair(&texture->kvDataHead, KTX_WRITER_SCPARAMS_KEY); if (writerScParams.size() > 0) { // Options always contain a leading space assert(writerScParams[0] == ' '); ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_SCPARAMS_KEY, static_cast(writerScParams.size()), writerScParams.c_str() + 1); // +1 to exclude leading space } // Save output file const auto outputPath = std::filesystem::path(DecodeUTF8Path(options.outputFilepath)); if (outputPath.has_parent_path()) std::filesystem::create_directories(outputPath.parent_path()); OutputStream outputFile(options.outputFilepath, *this); outputFile.writeKTX2(texture, *this); } } // namespace ktx KTX_COMMAND_ENTRY_POINT(ktxEncode, ktx::CommandEncode)