Files
how-to-vulkan/ktx/tools/ktx/command_encode.cpp
T
2026-06-14 19:09:18 +01:00

313 lines
13 KiB
C++

// 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 <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unordered_map>
#include <cxxopts.hpp>
#include <fmt/ostream.h>
#include <fmt/printf.h>
// -------------------------------------------------------------------------------------------------
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).
<!--For ASTC HDR formats the input file must be TBD (e.g. R16_{,S}FLOAT,
R16G16_{,S}FLOAT ...
-->
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
<!--Specifying both @e \--codec and @e \--format options is an error.
-->
The following options are available:
<dl>
<dt>\--codec basis-lz | uastc</dt>
<dd>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.</dd>
@snippet{doc} ktx/encode_utils_basis.h command options_basis_encoders
<dt>\--format</dt>
<dd>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.
</dl>
@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<OptionsEncode, OptionsEncodeASTC, OptionsEncodeBasis<true>, 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<std::string>(), "<enum>")
(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<std::string>(), "<target>");
}
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<std::string>();
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<decltype(options)>(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<std::streambuf*> 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<uint32_t>(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<uint32_t>(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)