diff --git a/tests/cts b/tests/cts index 3bb8db3b92..f1ff3936ce 160000 --- a/tests/cts +++ b/tests/cts @@ -1 +1 @@ -Subproject commit 3bb8db3b922e32b1e2e7c8da1935dca083c545ff +Subproject commit f1ff3936ce6908650206d2a1f93c4fc153174e7b diff --git a/tools/ktx/command_create.cpp b/tools/ktx/command_create.cpp index 29c9a316b6..11560e8eeb 100644 --- a/tools/ktx/command_create.cpp +++ b/tools/ktx/command_create.cpp @@ -525,10 +525,11 @@ struct OptionsCreate { }; struct OptionsASTC : public ktxAstcParams { - bool astc = false; + std::string astcOptions{}; + bool encodeASTC = false; ClampedOption qualityLevel{ktxAstcParams::qualityLevel, 0, KTX_PACK_ASTC_QUALITY_LEVEL_MAX}; - OptionsASTC() { + OptionsASTC() : ktxAstcParams() { threadCount = std::thread::hardware_concurrency(); if (threadCount == 0) threadCount = 1; @@ -566,9 +567,20 @@ struct OptionsASTC : public ktxAstcParams { "currently only available for normal maps and RGB color data."); } + void captureASTCOption(const char* name) { + astcOptions += fmt::format(" --{}", name); + } + + template + T captureASTCOption(cxxopts::ParseResult& args, const char* name) { + const T value = args[name].as(); + astcOptions += fmt::format(" --{} {}", name, value); + return value; + } + void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) { if (args["astc-mode"].count()) { - const auto modeStr = args["astc-mode"].as(); + const auto modeStr = to_lower_copy(captureASTCOption(args, "astc-mode")); if (modeStr == "ldr") mode = KTX_PACK_ASTC_ENCODER_MODE_LDR; else if (modeStr == "hdr") @@ -587,7 +599,7 @@ struct OptionsASTC : public ktxAstcParams { {"thorough", KTX_PACK_ASTC_QUALITY_LEVEL_THOROUGH}, {"exhaustive", KTX_PACK_ASTC_QUALITY_LEVEL_EXHAUSTIVE} }; - const auto qualityLevelStr = to_lower_copy(args["astc-quality"].as()); + const auto qualityLevelStr = to_lower_copy(captureASTCOption(args, "astc-quality")); const auto it = astc_quality_mapping.find(qualityLevelStr); if (it == astc_quality_mapping.end()) report.fatal_usage("Invalid astc-quality: \"{}\"", qualityLevelStr); @@ -596,7 +608,10 @@ struct OptionsASTC : public ktxAstcParams { qualityLevel = KTX_PACK_ASTC_QUALITY_LEVEL_MEDIUM; } - perceptual = args["astc-perceptual"].as(); + if (args["astc-perceptual"].count()) { + captureASTCOption("astc-perceptual"); + perceptual = KTX_TRUE; + } } }; @@ -911,7 +926,7 @@ void CommandCreate::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& fatal_usage("--compare-psnr can only be used with BasisLZ or UASTC encoding."); if (isFormatAstc(options.vkFormat) && !options.raw) { - options.astc = true; + options.encodeASTC = true; switch (options.vkFormat) { case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: [[fallthrough]]; @@ -976,7 +991,7 @@ void CommandCreate::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& } } - if (options._1d && options.astc) + if (options._1d && options.encodeASTC) fatal_usage("ASTC format {} cannot be used for 1 dimensional textures (indicated by --1d).", toString(options.vkFormat)); } @@ -1107,7 +1122,7 @@ void CommandCreate::executeCreate() { fatal_usage("Requested {} levels is too many. With input image \"{}\" sized {}x{} and depth {} the texture can only have {} levels at most.", options.levels.value_or(1), fmtInFile(inputFilepath), target.width(), target.height(), numBaseDepths, maxLevels); - if (options.astc) + if (options.encodeASTC) selectASTCMode(inputImageFile->spec().format().largestChannelBitLength()); firstImageSpec = inputImageFile->spec(); @@ -1202,6 +1217,16 @@ void CommandCreate::executeCreate() { encodeASTC(texture, options); compress(texture, options); + // Add KTXwriterScParams metadata if ASTC encoding, BasisU encoding, or other supercompression was used + const auto writerScParams = fmt::format("{}{}{}", options.astcOptions, options.codecOptions, options.compressOptions); + 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 if (std::filesystem::path(options.outputFilepath).has_parent_path()) std::filesystem::create_directories(std::filesystem::path(options.outputFilepath).parent_path()); @@ -1227,7 +1252,7 @@ void CommandCreate::encode(KTXTexture2& texture, OptionsCodec& opts) { } void CommandCreate::encodeASTC(KTXTexture2& texture, OptionsASTC& opts) { - if (opts.astc) { + 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)); diff --git a/tools/ktx/command_encode.cpp b/tools/ktx/command_encode.cpp index 00ca6dbc31..da9fc7f4e7 100644 --- a/tools/ktx/command_encode.cpp +++ b/tools/ktx/command_encode.cpp @@ -208,6 +208,16 @@ void CommandEncode::executeEncode() { fatal(rc::IO_FAILURE, "ZLIB deflation failed. KTX Error: {}", ktxErrorString(ret)); } + // Add KTXwriterScParams metadata + const auto writerScParams = fmt::format("{}{}", options.codecOptions, options.compressOptions); + 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 if (std::filesystem::path(options.outputFilepath).has_parent_path()) std::filesystem::create_directories(std::filesystem::path(options.outputFilepath).parent_path()); diff --git a/tools/ktx/compress_utils.h b/tools/ktx/compress_utils.h index 2ce27aefa0..878e2599e9 100644 --- a/tools/ktx/compress_utils.h +++ b/tools/ktx/compress_utils.h @@ -33,6 +33,7 @@ namespace ktx { //! [command options_compress] */ struct OptionsCompress { + std::string compressOptions{}; std::optional zstd; std::optional zlib; @@ -51,14 +52,21 @@ struct OptionsCompress { cxxopts::value(), ""); } + template + T captureCompressOption(cxxopts::ParseResult& args, const char* name) { + const T value = args[name].as(); + compressOptions += fmt::format(" --{} {}", name, value); + return value; + } + void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) { if (args["zstd"].count()) { - zstd = args["zstd"].as(); + zstd = captureCompressOption(args, "zstd"); if (zstd < 1u || zstd > 22u) report.fatal_usage("Invalid zstd level: \"{}\". Value must be between 1 and 22 inclusive.", zstd.value()); } if (args["zlib"].count()) { - zlib = args["zlib"].as(); + zlib = captureCompressOption(args, "zlib"); if (zlib < 1u || zlib > 9u) report.fatal_usage("Invalid zlib level: \"{}\". Value must be between 1 and 9 inclusive.", zlib.value()); } diff --git a/tools/ktx/encode_utils.h b/tools/ktx/encode_utils.h index d3f1fc079d..2a58f5adf8 100644 --- a/tools/ktx/encode_utils.h +++ b/tools/ktx/encode_utils.h @@ -245,6 +245,7 @@ struct OptionsCodec { } }; + std::string codecOptions{}; std::string codecName; EncodeCodec codec; BasisOptions basisOpts; @@ -334,6 +335,17 @@ struct OptionsCodec { } } + void captureCodecOption(const char* name) { + codecOptions += fmt::format(" --{}", name); + } + + template + T captureCodecOption(cxxopts::ParseResult& args, const char* name) { + const T value = args[name].as(); + codecOptions += fmt::format(" --{} {}", name, value); + return value; + } + void validateCommonEncodeArg(Reporter& report, const char* name) { if (codec == EncodeCodec::NONE) report.fatal(rc::INVALID_ARGUMENTS, @@ -418,47 +430,49 @@ struct OptionsCodec { if (args["clevel"].count()) { validateBasisLZArg(report, "clevel"); - basisOpts.compressionLevel = args["clevel"].as(); + basisOpts.compressionLevel = captureCodecOption(args, "clevel");; } if (args["qlevel"].count()) { validateBasisLZArg(report, "qlevel"); - basisOpts.qualityLevel = args["qlevel"].as(); + basisOpts.qualityLevel = captureCodecOption(args, "qlevel"); } if (args["no-endpoint-rdo"].count()) { validateBasisLZArg(report, "no-endpoint-rdo"); + captureCodecOption("no-endpoint-rdo"); basisOpts.noEndpointRDO = 1; } if (args["no-selector-rdo"].count()) { validateBasisLZArg(report, "no-selector-rdo"); + captureCodecOption("no-selector-rdo"); basisOpts.noSelectorRDO = 1; } if (args["max-endpoints"].count()) { validateBasisLZEndpointRDOArg(report, "max-endpoints"); - basisOpts.maxEndpoints = args["max-endpoints"].as(); + basisOpts.maxEndpoints = captureCodecOption(args, "max-endpoints"); } if (args["endpoint-rdo-threshold"].count()) { validateBasisLZEndpointRDOArg(report, "endpoint-rdo-threshold"); - basisOpts.endpointRDOThreshold = args["endpoint-rdo-threshold"].as(); + basisOpts.endpointRDOThreshold = captureCodecOption(args, "endpoint-rdo-threshold"); } if (args["max-selectors"].count()) { validateBasisLZSelectorRDOArg(report, "max-selectors"); - basisOpts.maxSelectors = args["max-selectors"].as(); + basisOpts.maxSelectors = captureCodecOption(args, "max-selectors"); } if (args["selector-rdo-threshold"].count()) { validateBasisLZSelectorRDOArg(report, "selector-rdo-threshold"); - basisOpts.selectorRDOThreshold = args["selector-rdo-threshold"].as(); + basisOpts.selectorRDOThreshold = captureCodecOption(args, "selector-rdo-threshold"); } if (args["uastc-quality"].count()) { validateUASTCArg(report, "uastc-quality"); - uint32_t level = args["uastc-quality"].as(); + uint32_t level = captureCodecOption(args, "uastc-quality"); level = std::clamp(level, 0, KTX_PACK_UASTC_MAX_LEVEL); basisOpts.uastcFlags = (unsigned int)~KTX_PACK_UASTC_LEVEL_MASK; basisOpts.uastcFlags |= level; @@ -466,53 +480,58 @@ struct OptionsCodec { if (args["uastc-rdo"].count()) { validateUASTCArg(report, "uastc-rdo"); + captureCodecOption("uastc-rdo"); basisOpts.uastcRDO = 1; } if (args["uastc-rdo-l"].count()) { validateUASTCRDOArg(report, "uastc-rdo-l"); - basisOpts.uastcRDOQualityScalar = args["uastc-rdo-l"].as(); + basisOpts.uastcRDOQualityScalar = captureCodecOption(args, "uastc-rdo-l"); } if (args["uastc-rdo-d"].count()) { validateUASTCRDOArg(report, "uastc-rdo-d"); - basisOpts.uastcRDODictSize = args["uastc-rdo-d"].as(); + basisOpts.uastcRDODictSize = captureCodecOption(args, "uastc-rdo-d"); } if (args["uastc-rdo-b"].count()) { validateUASTCRDOArg(report, "uastc-rdo-b"); - basisOpts.uastcRDOMaxSmoothBlockErrorScale = args["uastc-rdo-b"].as(); + basisOpts.uastcRDOMaxSmoothBlockErrorScale = captureCodecOption(args, "uastc-rdo-b"); } if (args["uastc-rdo-s"].count()) { validateUASTCRDOArg(report, "uastc-rdo-s"); - basisOpts.uastcRDOMaxSmoothBlockStdDev = args["uastc-rdo-s"].as(); + basisOpts.uastcRDOMaxSmoothBlockStdDev = captureCodecOption(args, "uastc-rdo-s"); } if (args["uastc-rdo-f"].count()) { validateUASTCRDOArg(report, "uastc-rdo-f"); + captureCodecOption("uastc-rdo-f"); basisOpts.uastcRDODontFavorSimplerModes = 1; } if (args["uastc-rdo-m"].count()) { validateUASTCRDOArg(report, "uastc-rdo-m"); + captureCodecOption("uastc-rdo-m"); basisOpts.uastcRDONoMultithreading = 1; } if (args["normal-mode"].count()) { validateCommonEncodeArg(report, "normal-mode"); + captureCodecOption("normal-mode"); basisOpts.normalMap = true; } if (args["threads"].count()) { validateCommonEncodeArg(report, "threads"); - basisOpts.threadCount = args["threads"].as(); + basisOpts.threadCount = captureCodecOption(args, "threads"); } else { basisOpts.threadCount = std::thread::hardware_concurrency(); } if (args["no-sse"].count()) { validateCommonEncodeArg(report, "no-sse"); + captureCodecOption("no-sse"); basisOpts.noSSE = true; } }