mirror of
https://github.com/KhronosGroup/KTX-Software.git
synced 2026-01-18 17:41:19 +01:00
Some minor documentation fixes: - Wrong link, `isCubemap` should be `isCompressed`:7a45c4e615/include/ktx.h (L329)- Unnecessary `n` at7a45c4e615/include/ktx.h (L341)- Typo: "betweeb" should be "between" at7a45c4e615/lib/texture.c (L831)- Unnecessary `<` at7a45c4e615/tools/ktx/command_create.cpp (L765)- The link to pyktx is missing `../` in7a45c4e615/pkgdoc/toolsDoxyLayout.xml (L41)and related files (I.e. on https://github.khronos.org/KTX-Software/libktx/index.html , the link to pyktx is a 404), see https://github.com/search?q=repo%3AKhronosGroup%2FKTX-Software%20%22pyktx%20Reference%22&type=code
2237 lines
104 KiB
C++
2237 lines
104 KiB
C++
// Copyright 2022-2023 The Khronos Group Inc.
|
|
// Copyright 2022-2023 RasterGrid Kft.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#include "command.h"
|
|
#include "platform_utils.h"
|
|
#include "metrics_utils.h"
|
|
#include "compress_utils.h"
|
|
#include "encode_utils.h"
|
|
#include "format_descriptor.h"
|
|
#include "formats.h"
|
|
#include "utility.h"
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <cxxopts.hpp>
|
|
#include <fmt/ostream.h>
|
|
#include <fmt/printf.h>
|
|
#include "ktx.h"
|
|
#include "image.hpp"
|
|
#include "imageio.h"
|
|
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
namespace ktx {
|
|
|
|
struct ColorSpaceInfo {
|
|
khr_df_transfer_e usedInputTransferFunction;
|
|
khr_df_primaries_e usedInputPrimaries;
|
|
std::unique_ptr<const TransferFunction> srcTransferFunction{};
|
|
std::unique_ptr<const TransferFunction> dstTransferFunction{};
|
|
std::unique_ptr<const ColorPrimaries> srcColorPrimaries{};
|
|
std::unique_ptr<const ColorPrimaries> dstColorPrimaries{};
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
struct OptionsCreate {
|
|
inline static const char* kFormat = "format";
|
|
inline static const char* k1D = "1d";
|
|
inline static const char* kCubemap = "cubemap";
|
|
inline static const char* kRaw = "raw";
|
|
inline static const char* kWidth = "width";
|
|
inline static const char* kHeight = "height";
|
|
inline static const char* kDepth = "depth";
|
|
inline static const char* kLayers = "layers";
|
|
inline static const char* kLevels = "levels";
|
|
inline static const char* kRuntimeMipmap = "runtime-mipmap";
|
|
inline static const char* kGenerateMipmap = "generate-mipmap";
|
|
inline static const char* kEncode = "encode";
|
|
inline static const char* kSwizzle = "swizzle";
|
|
inline static const char* kInputSwizzle = "input-swizzle";
|
|
inline static const char* kAssignOetf = "assign-oetf";
|
|
inline static const char* kAssignPrimaries = "assign-primaries";
|
|
inline static const char* kConvertOetf = "convert-oetf";
|
|
inline static const char* kConvertPrimaries = "convert-primaries";
|
|
inline static const char* kFailOnColorConversions = "fail-on-color-conversions";
|
|
inline static const char* kWarnOnColorConversions = "warn-on-color-conversions";
|
|
inline static const char* kMipmapFilter = "mipmap-filter";
|
|
inline static const char* kMipmapFilterScale = "mipmap-filter-scale";
|
|
inline static const char* kMipmapWrap = "mipmap-wrap";
|
|
|
|
bool _1d = false;
|
|
bool cubemap = false;
|
|
|
|
VkFormat vkFormat = VK_FORMAT_UNDEFINED;
|
|
FormatDescriptor formatDesc;
|
|
bool raw = false;
|
|
|
|
std::optional<uint32_t> width;
|
|
std::optional<uint32_t> height;
|
|
std::optional<uint32_t> depth;
|
|
std::optional<uint32_t> layers;
|
|
std::optional<uint32_t> levels;
|
|
|
|
bool mipmapRuntime = false;
|
|
bool mipmapGenerate = false;
|
|
std::optional<std::string> mipmapFilter;
|
|
std::string defaultMipmapFilter = "lanczos4";
|
|
std::optional<float> mipmapFilterScale;
|
|
float defaultMipmapFilterScale = 1.0f;
|
|
std::optional<basisu::Resampler::Boundary_Op> mipmapWrap;
|
|
basisu::Resampler::Boundary_Op defaultMipmapWrap = basisu::Resampler::Boundary_Op::BOUNDARY_WRAP;
|
|
std::optional<std::string> swizzle; /// Sets KTXswizzle
|
|
std::optional<std::string> swizzleInput; /// Used to swizzle the input image data
|
|
|
|
std::optional<khr_df_transfer_e> convertOETF = {};
|
|
std::optional<khr_df_transfer_e> assignOETF = {};
|
|
std::optional<khr_df_primaries_e> assignPrimaries = {};
|
|
std::optional<khr_df_primaries_e> convertPrimaries = {};
|
|
bool failOnColorConversions = false;
|
|
bool warnOnColorConversions = false;
|
|
|
|
void init(cxxopts::Options& opts) {
|
|
opts.add_options()
|
|
(kFormat, "KTX format enum that specifies the image data format."
|
|
" The enum names are matching the VkFormats without the VK_FORMAT_ prefix."
|
|
" The VK_FORMAT_ prefix is ignored if present."
|
|
"\nWhen used with --encode it specifies the target format before the encoding step."
|
|
" In this case it must be one of:"
|
|
"\n R8_UNORM"
|
|
"\n R8_SRGB"
|
|
"\n R8G8_UNORM"
|
|
"\n R8G8_SRGB"
|
|
"\n R8G8B8_UNORM"
|
|
"\n R8G8B8_SRGB"
|
|
"\n R8G8B8A8_UNORM"
|
|
"\n R8G8B8A8_SRGB"
|
|
"\nIf the format is an ASTC format the ASTC encoder specific options become valid,"
|
|
" otherwise they are ignored."
|
|
"\nThe format will be used to verify and load all input files into a texture before encoding."
|
|
" Case insensitive. Required.", cxxopts::value<std::string>(), "<enum>")
|
|
(k1D, "Create a 1D texture. If not set the texture will be a 2D or 3D texture.")
|
|
(kCubemap, "Create a cubemap texture. If not set the texture will be a 2D or 3D texture.")
|
|
(kRaw, "Create from raw image data.")
|
|
(kWidth, "Base level width in pixels.", cxxopts::value<uint32_t>(), "[0-9]+")
|
|
(kHeight, "Base level height in pixels.", cxxopts::value<uint32_t>(), "[0-9]+")
|
|
(kDepth, "Base level depth in pixels. If set the texture will be a 3D texture.", cxxopts::value<uint32_t>(), "[0-9]+")
|
|
(kLayers, "Number of layers. If set the texture will be an array texture.", cxxopts::value<uint32_t>(), "[0-9]+")
|
|
(kLevels, "Number of mip levels.", cxxopts::value<uint32_t>(), "[0-9]+")
|
|
(kRuntimeMipmap, "Runtime mipmap generation mode.")
|
|
(kGenerateMipmap, "Causes mipmaps to be generated during texture creation."
|
|
" It enables the use of \'Generate Mipmap\' options."
|
|
" If the --levels is not specified the maximum possible mip level will be generated."
|
|
" This option is mutually exclusive with --runtime-mipmap and cannot be used with UINT or 3D textures.")
|
|
(kEncode, "Encode the created KTX file. Case insensitive."
|
|
"\nPossible options are: basis-lz | uastc", cxxopts::value<std::string>(), "<codec>")
|
|
(kSwizzle, "KTX swizzle metadata.", cxxopts::value<std::string>(), "[rgba01]{4}")
|
|
(kInputSwizzle, "Pre-swizzle input channels.", cxxopts::value<std::string>(), "[rgba01]{4}")
|
|
(kAssignOetf, "Force the created texture to have the specified transfer function, ignoring"
|
|
" the transfer function of the input file(s). Case insensitive."
|
|
"\nPossible options are: linear | srgb", cxxopts::value<std::string>(), "<oetf>")
|
|
(kAssignPrimaries, "Force the created texture to have the specified color primaries, ignoring"
|
|
" the color primaries of the input file(s). Case insensitive."
|
|
"\nPossible options are:"
|
|
" none | bt709 | srgb | bt601-ebu | bt601-smpte | bt2020 | ciexyz | aces | acescc | ntsc1953 | pal525 | displayp3 | adobergb.",
|
|
cxxopts::value<std::string>(), "<primaries>")
|
|
(kConvertOetf, "Convert the input image(s) to the specified transfer function, if different"
|
|
" from the transfer function of the input file(s). If both this and --assign-oetf are specified,"
|
|
" conversion will be performed from the assigned transfer function to the transfer function"
|
|
" specified by this option, if different. Case insensitive."
|
|
"\nPossible options are: linear | srgb", cxxopts::value<std::string>(), "<oetf>")
|
|
(kConvertPrimaries, "Convert the image image(s) to the specified color primaries, if different"
|
|
" from the color primaries of the input file(s) or the one specified by --assign-primaries."
|
|
" If both this and --assign-primaries are specified, conversion will be performed from "
|
|
" the assigned primaries to the primaries specified by this option, if different."
|
|
" This option is not allowed to be specified when --assign-primaries is set to 'none'."
|
|
" Case insensitive."
|
|
"\nPossible options are:"
|
|
" bt709 | srgb | bt601-ebu | bt601-smpte | bt2020 | ciexyz | aces | acescc | ntsc1953 | pal525 | displayp3 | adobergb.",
|
|
cxxopts::value<std::string>(), "<primaries>")
|
|
(kFailOnColorConversions, "Generates an error if any of the input images would need to be color converted.")
|
|
(kWarnOnColorConversions, "Generates a warning if any of the input images are color converted.");
|
|
|
|
opts.add_options("Generate Mipmap")
|
|
(kMipmapFilter, "Specifies the filter to use when generating the mipmaps. Case insensitive."
|
|
"\nPossible options are:"
|
|
" box | tent | bell | b-spline | mitchell | blackman | lanczos3 | lanczos4 | lanczos6 |"
|
|
" lanczos12 | kaiser | gaussian | catmullrom | quadratic_interp | quadratic_approx | "
|
|
" quadratic_mix."
|
|
" Defaults to lanczos4.",
|
|
cxxopts::value<std::string>(), "<filter>")
|
|
(kMipmapFilterScale, "The filter scale to use. Defaults to 1.0.", cxxopts::value<float>(), "<float>")
|
|
(kMipmapWrap, "Specify how to sample pixels near the image boundaries. Case insensitive."
|
|
"\nPossible options are:"
|
|
" wrap | reflect | clamp."
|
|
" Defaults to clamp.", cxxopts::value<std::string>(), "<mode>");
|
|
}
|
|
|
|
std::optional<khr_df_transfer_e> parseTransferFunction(cxxopts::ParseResult& args, const char* argName, Reporter& report) const {
|
|
static const std::unordered_map<std::string, khr_df_transfer_e> values{
|
|
{ "LINEAR", KHR_DF_TRANSFER_LINEAR },
|
|
{ "SRGB", KHR_DF_TRANSFER_SRGB }
|
|
};
|
|
|
|
std::optional<khr_df_transfer_e> result = {};
|
|
|
|
if (args[argName].count()) {
|
|
const auto oetfStr = args[argName].as<std::string>();
|
|
const auto it = values.find(to_upper_copy(oetfStr));
|
|
if (it != values.end()) {
|
|
result = it->second;
|
|
} else {
|
|
report.fatal_usage("Invalid or unsupported transfer function specified as --{} argument: \"{}\".", argName, oetfStr);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::optional<khr_df_primaries_e> parseColorPrimaries(cxxopts::ParseResult& args, const char* argName, Reporter& report) const {
|
|
static const std::unordered_map<std::string, khr_df_primaries_e> values{
|
|
{ "NONE", KHR_DF_PRIMARIES_UNSPECIFIED },
|
|
{ "BT709", KHR_DF_PRIMARIES_BT709 },
|
|
{ "SRGB", KHR_DF_PRIMARIES_SRGB },
|
|
{ "BT601-EBU", KHR_DF_PRIMARIES_BT601_EBU },
|
|
{ "BT601-SMPTE", KHR_DF_PRIMARIES_BT601_SMPTE },
|
|
{ "BT2020", KHR_DF_PRIMARIES_BT2020 },
|
|
{ "CIEXYZ", KHR_DF_PRIMARIES_CIEXYZ },
|
|
{ "ACES", KHR_DF_PRIMARIES_ACES },
|
|
{ "ACESCC", KHR_DF_PRIMARIES_ACESCC },
|
|
{ "NTSC1953", KHR_DF_PRIMARIES_NTSC1953 },
|
|
{ "PAL525", KHR_DF_PRIMARIES_PAL525 },
|
|
{ "DISPLAYP3", KHR_DF_PRIMARIES_DISPLAYP3 },
|
|
{ "ADOBERGB", KHR_DF_PRIMARIES_ADOBERGB },
|
|
};
|
|
|
|
std::optional<khr_df_primaries_e> result = {};
|
|
|
|
if (args[argName].count()) {
|
|
const auto primariesStr = args[argName].as<std::string>();
|
|
const auto it = values.find(to_upper_copy(primariesStr));
|
|
if (it != values.end()) {
|
|
result = it->second;
|
|
} else {
|
|
report.fatal_usage("Invalid or unsupported primaries specified as --{} argument: \"{}\".", argName, primariesStr);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) {
|
|
_1d = args[k1D].as<bool>();
|
|
cubemap = args[kCubemap].as<bool>();
|
|
raw = args[kRaw].as<bool>();
|
|
|
|
if (args[kWidth].count())
|
|
width = args[kWidth].as<uint32_t>();
|
|
if (args[kHeight].count())
|
|
height = args[kHeight].as<uint32_t>();
|
|
if (args[kDepth].count())
|
|
depth = args[kDepth].as<uint32_t>();
|
|
if (args[kLayers].count())
|
|
layers = args[kLayers].as<uint32_t>();
|
|
if (args[kLevels].count())
|
|
levels = args[kLevels].as<uint32_t>();
|
|
|
|
mipmapRuntime = args[kRuntimeMipmap].as<bool>();
|
|
mipmapGenerate = args[kGenerateMipmap].as<bool>();
|
|
|
|
if (args[kMipmapFilter].count()) {
|
|
static const std::unordered_set<std::string> filter_table{
|
|
"box",
|
|
"tent",
|
|
"bell",
|
|
"b-spline",
|
|
"mitchell",
|
|
"blackman",
|
|
"lanczos3",
|
|
"lanczos4",
|
|
"lanczos6",
|
|
"lanczos12",
|
|
"kaiser",
|
|
"gaussian",
|
|
"catmullrom",
|
|
"quadratic_interp",
|
|
"quadratic_approx",
|
|
"quadratic_mix",
|
|
};
|
|
|
|
mipmapFilter = to_lower_copy(args[kMipmapFilter].as<std::string>());
|
|
if (filter_table.count(*mipmapFilter) == 0)
|
|
report.fatal_usage("Invalid or unsupported mipmap filter specified as --mipmap-filter argument: \"{}\".", *mipmapFilter);
|
|
}
|
|
|
|
if (args[kMipmapFilterScale].count())
|
|
mipmapFilterScale = args[kMipmapFilterScale].as<float>();
|
|
|
|
if (args[kMipmapWrap].count()) {
|
|
static const std::unordered_map<std::string, basisu::Resampler::Boundary_Op> wrap_table{
|
|
{ "clamp", basisu::Resampler::Boundary_Op::BOUNDARY_CLAMP },
|
|
{ "wrap", basisu::Resampler::Boundary_Op::BOUNDARY_WRAP },
|
|
{ "reflect", basisu::Resampler::Boundary_Op::BOUNDARY_REFLECT },
|
|
};
|
|
|
|
const auto wrapStr = to_lower_copy(args[kMipmapWrap].as<std::string>());
|
|
const auto it = wrap_table.find(wrapStr);
|
|
if (it == wrap_table.end())
|
|
report.fatal_usage("Invalid or unsupported mipmap wrap mode specified as --mipmap-wrap argument: \"{}\".", wrapStr);
|
|
else
|
|
mipmapWrap = it->second;
|
|
}
|
|
|
|
if (args[kSwizzle].count()) {
|
|
swizzle = to_lower_copy(args[kSwizzle].as<std::string>());
|
|
const auto errorFmt = "Invalid --swizzle value: \"{}\". The value must match the \"[rgba01]{{4}}\" regex.";
|
|
if (swizzle->size() != 4)
|
|
report.fatal_usage(errorFmt, *swizzle);
|
|
for (const auto c : *swizzle)
|
|
if (!contains("rgba01", c))
|
|
report.fatal_usage(errorFmt, *swizzle);
|
|
}
|
|
if (args[kInputSwizzle].count()) {
|
|
swizzleInput = to_lower_copy(args[kInputSwizzle].as<std::string>());
|
|
const auto errorFmt = "Invalid --input-swizzle value: \"{}\". The value must match the \"[rgba01]{{4}}\" regex.";
|
|
if (swizzleInput->size() != 4)
|
|
report.fatal_usage(errorFmt, *swizzleInput);
|
|
for (const auto c : *swizzleInput)
|
|
if (!contains("rgba01", c))
|
|
report.fatal_usage(errorFmt, *swizzleInput);
|
|
}
|
|
|
|
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;
|
|
} else {
|
|
report.fatal_usage("Required option 'format' is missing.");
|
|
}
|
|
|
|
// List of formats that have supported format conversions
|
|
static const std::unordered_set<VkFormat> convertableFormats{
|
|
VK_FORMAT_R8_UNORM,
|
|
VK_FORMAT_R8_SRGB,
|
|
VK_FORMAT_R8G8_UNORM,
|
|
VK_FORMAT_R8G8_SRGB,
|
|
VK_FORMAT_R8G8B8_UNORM,
|
|
VK_FORMAT_R8G8B8_SRGB,
|
|
VK_FORMAT_B8G8R8_UNORM,
|
|
VK_FORMAT_B8G8R8_SRGB,
|
|
VK_FORMAT_R8G8B8A8_UNORM,
|
|
VK_FORMAT_R8G8B8A8_SRGB,
|
|
VK_FORMAT_B8G8R8A8_UNORM,
|
|
VK_FORMAT_B8G8R8A8_SRGB,
|
|
VK_FORMAT_A8B8G8R8_UNORM_PACK32,
|
|
VK_FORMAT_A8B8G8R8_SRGB_PACK32,
|
|
VK_FORMAT_ASTC_4x4_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_4x4_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_5x4_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_5x4_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_5x5_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_5x5_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_6x5_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_6x5_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_6x6_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_6x6_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_8x5_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_8x5_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_8x6_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_8x6_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_8x8_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_8x8_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_10x5_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_10x5_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_10x6_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_10x6_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_10x8_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_10x8_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_10x10_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_10x10_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_12x10_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_12x10_SRGB_BLOCK,
|
|
VK_FORMAT_ASTC_12x12_UNORM_BLOCK,
|
|
VK_FORMAT_ASTC_12x12_SRGB_BLOCK,
|
|
VK_FORMAT_R4G4_UNORM_PACK8,
|
|
VK_FORMAT_R5G6B5_UNORM_PACK16,
|
|
VK_FORMAT_B5G6R5_UNORM_PACK16,
|
|
VK_FORMAT_R4G4B4A4_UNORM_PACK16,
|
|
VK_FORMAT_B4G4R4A4_UNORM_PACK16,
|
|
VK_FORMAT_R5G5B5A1_UNORM_PACK16,
|
|
VK_FORMAT_B5G5R5A1_UNORM_PACK16,
|
|
VK_FORMAT_A1R5G5B5_UNORM_PACK16,
|
|
VK_FORMAT_A4R4G4B4_UNORM_PACK16,
|
|
VK_FORMAT_A4B4G4R4_UNORM_PACK16,
|
|
VK_FORMAT_R10X6_UNORM_PACK16,
|
|
VK_FORMAT_R10X6G10X6_UNORM_2PACK16,
|
|
VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16,
|
|
VK_FORMAT_R12X4_UNORM_PACK16,
|
|
VK_FORMAT_R12X4G12X4_UNORM_2PACK16,
|
|
VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16,
|
|
VK_FORMAT_R16_UNORM,
|
|
VK_FORMAT_R16G16_UNORM,
|
|
VK_FORMAT_R16G16B16_UNORM,
|
|
VK_FORMAT_R16G16B16A16_UNORM,
|
|
VK_FORMAT_A2R10G10B10_UNORM_PACK32,
|
|
VK_FORMAT_A2B10G10R10_UNORM_PACK32,
|
|
VK_FORMAT_G8B8G8R8_422_UNORM,
|
|
VK_FORMAT_B8G8R8G8_422_UNORM,
|
|
VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16,
|
|
VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16,
|
|
VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16,
|
|
VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16,
|
|
VK_FORMAT_G16B16G16R16_422_UNORM,
|
|
VK_FORMAT_B16G16R16G16_422_UNORM,
|
|
VK_FORMAT_R8_UINT,
|
|
VK_FORMAT_R8_SINT,
|
|
VK_FORMAT_R16_UINT,
|
|
VK_FORMAT_R16_SINT,
|
|
VK_FORMAT_R32_UINT,
|
|
VK_FORMAT_R8G8_UINT,
|
|
VK_FORMAT_R8G8_SINT,
|
|
VK_FORMAT_R16G16_UINT,
|
|
VK_FORMAT_R16G16_SINT,
|
|
VK_FORMAT_R32G32_UINT,
|
|
VK_FORMAT_R8G8B8_UINT,
|
|
VK_FORMAT_R8G8B8_SINT,
|
|
VK_FORMAT_B8G8R8_UINT,
|
|
VK_FORMAT_B8G8R8_SINT,
|
|
VK_FORMAT_R16G16B16_UINT,
|
|
VK_FORMAT_R16G16B16_SINT,
|
|
VK_FORMAT_R32G32B32_UINT,
|
|
VK_FORMAT_R8G8B8A8_UINT,
|
|
VK_FORMAT_R8G8B8A8_SINT,
|
|
VK_FORMAT_B8G8R8A8_UINT,
|
|
VK_FORMAT_B8G8R8A8_SINT,
|
|
VK_FORMAT_A8B8G8R8_UINT_PACK32,
|
|
VK_FORMAT_A8B8G8R8_SINT_PACK32,
|
|
VK_FORMAT_R16G16B16A16_UINT,
|
|
VK_FORMAT_R16G16B16A16_SINT,
|
|
VK_FORMAT_R32G32B32A32_UINT,
|
|
VK_FORMAT_A2R10G10B10_UINT_PACK32,
|
|
VK_FORMAT_A2R10G10B10_SINT_PACK32,
|
|
VK_FORMAT_A2B10G10R10_SINT_PACK32,
|
|
VK_FORMAT_A2B10G10R10_UINT_PACK32,
|
|
VK_FORMAT_R16_SFLOAT,
|
|
VK_FORMAT_R16G16_SFLOAT,
|
|
VK_FORMAT_R16G16B16_SFLOAT,
|
|
VK_FORMAT_R16G16B16A16_SFLOAT,
|
|
VK_FORMAT_R32_SFLOAT,
|
|
VK_FORMAT_R32G32_SFLOAT,
|
|
VK_FORMAT_R32G32B32_SFLOAT,
|
|
VK_FORMAT_R32G32B32A32_SFLOAT,
|
|
VK_FORMAT_B10G11R11_UFLOAT_PACK32,
|
|
VK_FORMAT_E5B9G9R9_UFLOAT_PACK32,
|
|
VK_FORMAT_D16_UNORM,
|
|
VK_FORMAT_X8_D24_UNORM_PACK32,
|
|
VK_FORMAT_D32_SFLOAT,
|
|
VK_FORMAT_S8_UINT,
|
|
VK_FORMAT_D16_UNORM_S8_UINT,
|
|
VK_FORMAT_D24_UNORM_S8_UINT,
|
|
VK_FORMAT_D32_SFLOAT_S8_UINT,
|
|
VK_FORMAT_A8_UNORM_KHR,
|
|
VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR,
|
|
};
|
|
|
|
if (isProhibitedFormat(vkFormat))
|
|
report.fatal_usage("The requested {} format is prohibited in KTX files.", toString(vkFormat));
|
|
|
|
if (!raw && !convertableFormats.count(vkFormat))
|
|
report.fatal_usage("Unsupported format for non-raw create: {}.", toString(vkFormat));
|
|
|
|
if (raw) {
|
|
if (!width)
|
|
report.fatal_usage("Option --width is missing but is required for --raw texture creation.");
|
|
if (!height)
|
|
report.fatal_usage("Option --height is missing but is required for --raw texture creation.");
|
|
} else {
|
|
if (width)
|
|
report.warning("Option --width is ignored for non-raw texture creation.");
|
|
if (height)
|
|
report.warning("Option --height is ignored for non-raw texture creation.");
|
|
}
|
|
|
|
if (width == 0u)
|
|
report.fatal_usage("The --width cannot be 0.");
|
|
if (height == 0u)
|
|
report.fatal_usage("The --height cannot be 0.");
|
|
if (layers == 0u)
|
|
report.fatal_usage("The --layers cannot be 0.");
|
|
if (levels == 0u)
|
|
report.fatal_usage("The --levels cannot be 0.");
|
|
if (depth == 0u)
|
|
report.fatal_usage("The --depth cannot be 0.");
|
|
|
|
if (raw) {
|
|
const auto maxDimension = std::max(width.value_or(1), std::max(height.value_or(1), depth.value_or(1)));
|
|
const auto maxLevels = log2(maxDimension) + 1;
|
|
|
|
if (levels.value_or(1) > maxLevels)
|
|
report.fatal_usage("Requested {} levels is too many. With base size {}x{}x{} the texture can only have {} levels at most.",
|
|
levels.value_or(1), width.value_or(1), height.value_or(1), depth.value_or(1), maxLevels);
|
|
}
|
|
|
|
if (_1d && height && height != 1u)
|
|
report.fatal_usage("For --1d textures the --height must be 1.");
|
|
|
|
if (layers && depth)
|
|
report.fatal_usage("3D array texture creation is unsupported. --layers is {} and --depth is {}.",
|
|
*layers, *depth);
|
|
|
|
if (cubemap && depth)
|
|
report.fatal_usage("Cubemaps cannot have 3D textures. --depth is {}.", *depth);
|
|
|
|
if (mipmapRuntime && levels.value_or(1) > 1u)
|
|
report.fatal_usage("Conflicting options: --runtime-mipmap cannot be used with more than 1 --levels.");
|
|
|
|
if (mipmapGenerate && mipmapRuntime)
|
|
report.fatal_usage("Conflicting options: --generate-mipmap and --runtime-mipmap cannot be used together.");
|
|
|
|
if (mipmapGenerate && raw)
|
|
report.fatal_usage("Conflicting options: --generate-mipmap cannot be used with --raw.");
|
|
|
|
if (mipmapGenerate && depth)
|
|
report.fatal_usage("Mipmap generation for 3D textures is not supported: --generate-mipmap cannot be used with --depth.");
|
|
|
|
if (mipmapFilter && !mipmapGenerate)
|
|
report.fatal_usage("Option --mipmap-filter can only be used if --generate-mipmap is set.");
|
|
|
|
if (mipmapFilterScale && !mipmapGenerate)
|
|
report.fatal_usage("Option --mipmap-filter-scale can only be used if --generate-mipmap is set.");
|
|
|
|
if (mipmapWrap && !mipmapGenerate)
|
|
report.fatal_usage("Option --mipmap-wrap can only be used if --generate-mipmap is set.");
|
|
|
|
formatDesc = createFormatDescriptor(vkFormat, report);
|
|
|
|
convertOETF = parseTransferFunction(args, kConvertOetf, report);
|
|
assignOETF = parseTransferFunction(args, kAssignOetf, report);
|
|
|
|
convertPrimaries = parseColorPrimaries(args, kConvertPrimaries, report);
|
|
assignPrimaries = parseColorPrimaries(args, kAssignPrimaries, report);
|
|
|
|
if (convertPrimaries.has_value() && assignPrimaries == KHR_DF_PRIMARIES_UNSPECIFIED)
|
|
report.fatal_usage("Option --convert-primaries cannot be used when --assign-primaries is set to 'none'.");
|
|
|
|
if (raw) {
|
|
if (convertOETF.has_value())
|
|
report.fatal_usage("Option --convert-oetf cannot be used with --raw.");
|
|
if (convertPrimaries.has_value())
|
|
report.fatal_usage("Option --convert-primaries cannot be used with --raw.");
|
|
}
|
|
|
|
if (formatDesc.transfer() == KHR_DF_TRANSFER_SRGB) {
|
|
const auto error_message = "Invalid value to --{} \"{}\" for format \"{}\". Transfer function must be sRGB for sRGB formats.";
|
|
if (!convertOETF.has_value() && assignOETF.has_value()) {
|
|
switch (assignOETF.value()) {
|
|
case KHR_DF_TRANSFER_UNSPECIFIED:
|
|
case KHR_DF_TRANSFER_SRGB:
|
|
// assign-oetf must either not be specified or must be sRGB for an sRGB format
|
|
break;
|
|
default:
|
|
report.fatal_usage(error_message, "assign-oetf", args[kAssignOetf].as<std::string>(), args[kFormat].as<std::string>());
|
|
}
|
|
} else if (convertOETF.has_value() && convertOETF != KHR_DF_TRANSFER_SRGB) {
|
|
report.fatal_usage(error_message, "convert-oetf", args[kConvertOetf].as<std::string>(), args[kFormat].as<std::string>());
|
|
}
|
|
}
|
|
|
|
if (isFormatNotSRGBButHasSRGBVariant(vkFormat)) {
|
|
const auto error_message = "Invalid value to --{} \"{}\" for format \"{}\". Transfer function must not be sRGB for a non-sRGB VkFormat with sRGB variant.";
|
|
if (!convertOETF.has_value() && assignOETF.has_value() && assignOETF == KHR_DF_TRANSFER_SRGB) {
|
|
report.fatal_usage(error_message, "assign-oetf", args[kAssignOetf].as<std::string>(), args[kFormat].as<std::string>());
|
|
} else if (convertOETF.has_value() && convertOETF == KHR_DF_TRANSFER_SRGB) {
|
|
report.fatal_usage(error_message, "convert-oetf", args[kConvertOetf].as<std::string>(), args[kFormat].as<std::string>());
|
|
}
|
|
}
|
|
|
|
if (args[kFailOnColorConversions].count())
|
|
failOnColorConversions = true;
|
|
|
|
if (args[kWarnOnColorConversions].count()) {
|
|
if (failOnColorConversions)
|
|
report.fatal_usage("The options --fail-on-color-conversions and warn-on-color-conversions are mutually exclusive.");
|
|
warnOnColorConversions = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
struct OptionsASTC : public ktxAstcParams {
|
|
inline static const char* kAstcQuality = "astc-quality";
|
|
inline static const char* kAstcPerceptual = "astc-perceptual";
|
|
|
|
inline static const char* kAstcOptions[] = {
|
|
kAstcQuality,
|
|
kAstcPerceptual
|
|
};
|
|
|
|
std::string astcOptions{};
|
|
bool encodeASTC = false;
|
|
ClampedOption<ktx_uint32_t> qualityLevel{ktxAstcParams::qualityLevel, 0, KTX_PACK_ASTC_QUALITY_LEVEL_MAX};
|
|
|
|
OptionsASTC() : ktxAstcParams() {
|
|
threadCount = std::thread::hardware_concurrency();
|
|
if (threadCount == 0)
|
|
threadCount = 1;
|
|
structSize = sizeof(ktxAstcParams);
|
|
normalMap = false;
|
|
for (int i = 0; i < 4; i++)
|
|
inputSwizzle[i] = 0;
|
|
qualityLevel.clear();
|
|
}
|
|
|
|
void init(cxxopts::Options& opts) {
|
|
opts.add_options("Encode ASTC")
|
|
(kAstcQuality,
|
|
"The quality level configures the quality-performance tradeoff for "
|
|
"the compressor; more complete searches of the search space "
|
|
"improve image quality at the expense of compression time. Default "
|
|
"is 'medium'. The quality level can be set between fastest (0) and "
|
|
"exhaustive (100) via the following fixed quality presets:\n\n"
|
|
" Level | Quality\n"
|
|
" ---------- | -----------------------------\n"
|
|
" fastest | (equivalent to quality = 0)\n"
|
|
" fast | (equivalent to quality = 10)\n"
|
|
" medium | (equivalent to quality = 60)\n"
|
|
" thorough | (equivalent to quality = 98)\n"
|
|
" exhaustive | (equivalent to quality = 100)",
|
|
cxxopts::value<std::string>(), "<level>")
|
|
(kAstcPerceptual,
|
|
"The codec should optimize for perceptual error, instead of direct "
|
|
"RMS error. This aims to improve perceived image quality, but "
|
|
"typically lowers the measured PSNR score. Perceptual methods are "
|
|
"currently only available for normal maps and RGB color data.");
|
|
}
|
|
|
|
void captureASTCOption(const char* name) {
|
|
astcOptions += fmt::format(" --{}", name);
|
|
}
|
|
|
|
template <typename T>
|
|
T captureASTCOption(cxxopts::ParseResult& args, const char* name) {
|
|
const T value = args[name].as<T>();
|
|
astcOptions += fmt::format(" --{} {}", name, value);
|
|
return value;
|
|
}
|
|
|
|
void process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) {
|
|
if (args[kAstcQuality].count()) {
|
|
static std::unordered_map<std::string, ktx_pack_astc_quality_levels_e> astc_quality_mapping{
|
|
{"fastest", KTX_PACK_ASTC_QUALITY_LEVEL_FASTEST},
|
|
{"fast", KTX_PACK_ASTC_QUALITY_LEVEL_FAST},
|
|
{"medium", KTX_PACK_ASTC_QUALITY_LEVEL_MEDIUM},
|
|
{"thorough", KTX_PACK_ASTC_QUALITY_LEVEL_THOROUGH},
|
|
{"exhaustive", KTX_PACK_ASTC_QUALITY_LEVEL_EXHAUSTIVE}
|
|
};
|
|
const auto qualityLevelStr = to_lower_copy(captureASTCOption<std::string>(args, kAstcQuality));
|
|
const auto it = astc_quality_mapping.find(qualityLevelStr);
|
|
if (it == astc_quality_mapping.end())
|
|
report.fatal_usage("Invalid astc-quality: \"{}\"", qualityLevelStr);
|
|
qualityLevel = it->second;
|
|
} else {
|
|
qualityLevel = KTX_PACK_ASTC_QUALITY_LEVEL_MEDIUM;
|
|
}
|
|
|
|
if (args[kAstcPerceptual].count()) {
|
|
captureASTCOption(kAstcPerceptual);
|
|
perceptual = KTX_TRUE;
|
|
}
|
|
}
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
/** @page ktx_create ktx create
|
|
@~English
|
|
|
|
Create a KTX2 file from various input files.
|
|
|
|
@section ktx_create_synopsis SYNOPSIS
|
|
ktx create [option...] @e input-file... @e output-file
|
|
|
|
@section ktx_create_description DESCRIPTION
|
|
@b ktx @b create can create, encode and supercompress a KTX2 file from the
|
|
input images specified as the @e input-file... arguments and save it as the
|
|
@e output-file. The last positional argument is treated 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.
|
|
|
|
Each @e input-file must be a valid EXR (.exr), PNG (.png) or Raw (.raw) file.
|
|
PNG files with luminance (L) or luminance + alpha (LA) data will be converted
|
|
to RGB as LLL and RGBA as LLLA before processing further.
|
|
The input file formats must be compatible with the requested KTX format enum and
|
|
must have at least the same level of precision and number of channels.
|
|
Any unused channel will be discarded silently.
|
|
|
|
The number of input-files specified must match the expected number of input images
|
|
based on the used options.
|
|
|
|
The following options are available:
|
|
<dl>
|
|
<dt>\--format <enum></dt>
|
|
<dd>KTX format enum that specifies the image data format.
|
|
The enum names are matching the VkFormats without the VK_FORMAT_ prefix.
|
|
The VK_FORMAT_ prefix is ignored if present.<br />
|
|
When used with --encode it specifies the target format before the encoding step.
|
|
In this case it must be one of:
|
|
<ul>
|
|
<li>R8_UNORM</li>
|
|
<li>R8_SRGB</li>
|
|
<li>R8G8_UNORM</li>
|
|
<li>R8G8_SRGB</li>
|
|
<li>R8G8B8_UNORM</li>
|
|
<li>R8G8B8_SRGB</li>
|
|
<li>R8G8B8A8_UNORM</li>
|
|
<li>R8G8B8A8_SRGB</li>
|
|
</ul>
|
|
If the format is an ASTC format the ASTC encoder specific options become valid,
|
|
otherwise they are ignored.<br />
|
|
The format will be used to verify and load all input files into a texture before encoding.<br />
|
|
Case insensitive. Required.</dd>
|
|
<dl>
|
|
<dt>\--astc-quality <level></dt>
|
|
<dd>The quality level configures the quality-performance
|
|
tradeoff for the compressor; more complete searches of the
|
|
search space improve image quality at the expense of
|
|
compression time. Default is 'medium'. The quality level can be
|
|
set between fastest (0) and exhaustive (100) via the
|
|
following fixed quality presets:
|
|
<table>
|
|
<tr><th>Level </th> <th> Quality </th></tr>
|
|
<tr><td>fastest </td> <td>(equivalent to quality = 0) </td></tr>
|
|
<tr><td>fast </td> <td>(equivalent to quality = 10) </td></tr>
|
|
<tr><td>medium </td> <td>(equivalent to quality = 60) </td></tr>
|
|
<tr><td>thorough </td> <td>(equivalent to quality = 98) </td></tr>
|
|
<tr><td>exhaustive </td> <td>(equivalent to quality = 100) </td></tr>
|
|
</table>
|
|
</dd>
|
|
<dt>\--astc-perceptual</dt>
|
|
<dd>The codec should optimize for perceptual error, instead of
|
|
direct RMS error. This aims to improve perceived image quality,
|
|
but typically lowers the measured PSNR score. Perceptual
|
|
methods are currently only available for normal maps and RGB
|
|
color data.</dd>
|
|
</dl>
|
|
<dt>\--1d</dt>
|
|
<dd>Create a 1D texture. If not set the texture will be a 2D or 3D texture.</dd>
|
|
<dt>\--cubemap</dt>
|
|
<dd>Create a cubemap texture. If not set the texture will be a 2D or 3D texture.</dd>
|
|
<dt>\--raw</dt>
|
|
<dd>Create from raw image data.</dd>
|
|
<dt>\--width</dt>
|
|
<dd>Base level width in pixels.</dd>
|
|
<dt>\--height</dt>
|
|
<dd>Base level height in pixels.</dd>
|
|
<dt>\--depth</dt>
|
|
<dd>Base level depth in pixels.
|
|
If set the texture will be a 3D texture.</dd>
|
|
<dt>\--layers</dt>
|
|
<dd>Number of layers.
|
|
If set the texture will be an array texture.</dd>
|
|
<dt>\--runtime-mipmap</dt>
|
|
<dd>Runtime mipmap generation mode.
|
|
Sets up the texture to request the mipmaps to be generated by the client application at
|
|
runtime.</dd>
|
|
<dt>\--generate-mipmap</dt>
|
|
<dd>Causes mipmaps to be generated during texture creation.
|
|
If the --levels is not specified the maximum possible mip level will be generated.
|
|
This option is mutually exclusive with --runtime-mipmap and cannot be used with SINT,
|
|
UINT or 3D textures.</dd>
|
|
When set it enables the use of the following \'Generate Mipmap\' options.
|
|
<dl>
|
|
<dt>\--mipmap-filter <filter></dt>
|
|
<dd>Specifies the filter to use when generating the mipmaps. Case insensitive.<br />
|
|
Possible options are:
|
|
box | tent | bell | b-spline | mitchell | blackman | lanczos3 | lanczos4 | lanczos6 |
|
|
lanczos12 | kaiser | gaussian | catmullrom | quadratic_interp | quadratic_approx |
|
|
quadratic_mix.
|
|
Defaults to lanczos4.</dd>
|
|
<dt>\--mipmap-filter-scale <float></dt>
|
|
<dd>The filter scale to use.
|
|
Defaults to 1.0.</dd>
|
|
<dt>\--mipmap-wrap <mode></dt>
|
|
<dd>Specify how to sample pixels near the image boundaries. Case insensitive.<br />
|
|
Possible options are:
|
|
wrap | reflect | clamp.
|
|
Defaults to clamp.</dd>
|
|
</dl>
|
|
</dl>
|
|
<dl>
|
|
<dt>\--encode basis-lz | uastc</dt>
|
|
<dd>Encode the texture with the specified codec before saving it.
|
|
This option matches the functionality of the @ref ktx_encode "ktx encode" command.
|
|
With each encoding option the following encoder specific options become valid,
|
|
otherwise they are ignored. Case-insensitive.</dd>
|
|
|
|
@snippet{doc} ktx/encode_utils.h command options_codec
|
|
@snippet{doc} ktx/metrics_utils.h command options_metrics
|
|
</dl>
|
|
<dl>
|
|
<dt>\--swizzle [rgba01]{4}</dt>
|
|
<dd>KTX swizzle metadata.</dd>
|
|
<dt>\--input-swizzle [rgba01]{4}</dt>
|
|
<dd>Pre-swizzle input channels.</dd>
|
|
<dt>\--assign-oetf <oetf></dt>
|
|
<dd>Force the created texture to have the specified transfer function, ignoring
|
|
the transfer function of the input file(s). Case insensitive.
|
|
Possible options are:
|
|
linear | srgb
|
|
</dd>
|
|
<dt>\--assign-primaries <primaries></dt>
|
|
<dd>Force the created texture to have the specified color primaries, ignoring
|
|
the color primaries of the input file(s). Case insensitive.
|
|
Possible options are:
|
|
none | bt709 | srgb | bt601-ebu | bt601-smpte | bt2020 | ciexyz | aces | acescc |
|
|
ntsc1953 | pal525 | displayp3 | adobergb
|
|
</dd>
|
|
<dt>\--convert-oetf <oetf></dt>
|
|
<dd>Convert the input image(s) to the specified transfer function, if different
|
|
from the transfer function of the input file(s). If both this and --assign-oetf are
|
|
specified, conversion will be performed from the assigned transfer function to the
|
|
transfer function specified by this option, if different. Case insensitive.
|
|
Possible options are:
|
|
linear | srgb
|
|
</dd>
|
|
<dt>\--convert-primaries <primaries></dt>
|
|
<dd>Convert the image image(s) to the specified color primaries, if different
|
|
from the color primaries of the input file(s) or the one specified by --assign-primaries.
|
|
If both this and --assign-primaries are specified, conversion will be performed from
|
|
the assigned primaries to the primaries specified by this option, if different.
|
|
This option is not allowed to be specified when --assign-primaries is set to 'none'.
|
|
Case insensitive.
|
|
Possible options are:
|
|
bt709 | srgb | bt601-ebu | bt601-smpte | bt2020 | ciexyz | aces | acescc | ntsc1953 |
|
|
pal525 | displayp3 | adobergb
|
|
</dd>
|
|
<dt>\--fail-on-color-conversions</dt>
|
|
<dd>Generates an error if any of the input images would need to be color converted.</dd>
|
|
<dt>\--warn-on-color-conversions</dt>
|
|
<dd>Generates a warning if any of the input images are color converted.</dd>
|
|
</dl>
|
|
@snippet{doc} ktx/compress_utils.h command options_compress
|
|
@snippet{doc} ktx/command.h command options_generic
|
|
|
|
@section ktx_create_exitstatus EXIT STATUS
|
|
@snippet{doc} ktx/command.h command exitstatus
|
|
|
|
@section ktx_create_history HISTORY
|
|
|
|
@par Version 4.0
|
|
- Initial version
|
|
|
|
@section ktx_create_author AUTHOR
|
|
- Mátyás Császár [Vader], RasterGrid www.rastergrid.com
|
|
- Daniel Rákos, RasterGrid www.rastergrid.com
|
|
*/
|
|
class CommandCreate : public Command {
|
|
private:
|
|
Combine<OptionsCreate, OptionsASTC, OptionsCodec<false>, OptionsMetrics, OptionsCompress, OptionsMultiInSingleOut, OptionsGeneric> options;
|
|
|
|
uint32_t targetChannelCount = 0; // Derived from VkFormat
|
|
|
|
uint32_t numLevels = 0;
|
|
uint32_t numLayers = 0;
|
|
uint32_t numFaces = 0;
|
|
uint32_t baseDepth = 0;
|
|
|
|
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 executeCreate();
|
|
void encode(KTXTexture2& texture, OptionsCodec<false>& opts);
|
|
void encodeASTC(KTXTexture2& texture, OptionsASTC& opts);
|
|
void compress(KTXTexture2& texture, const OptionsCompress& opts);
|
|
|
|
private:
|
|
template <typename F>
|
|
void foreachImage(const FormatDescriptor& format, F&& func);
|
|
|
|
[[nodiscard]] KTXTexture2 createTexture(const ImageSpec& target);
|
|
void generateMipLevels(KTXTexture2& texture, std::unique_ptr<Image> image, ImageInput& inputFile,
|
|
uint32_t numMipLevels, uint32_t layerIndex, uint32_t faceIndex, uint32_t depthSliceIndex);
|
|
|
|
[[nodiscard]] std::string readRawFile(const std::filesystem::path& filepath);
|
|
[[nodiscard]] std::unique_ptr<Image> loadInputImage(ImageInput& inputImageFile);
|
|
std::vector<uint8_t> convert(const std::unique_ptr<Image>& image, VkFormat format, ImageInput& inputFile);
|
|
|
|
std::unique_ptr<const ColorPrimaries> createColorPrimaries(khr_df_primaries_e primaries) const;
|
|
|
|
void selectASTCMode(uint32_t bitLength);
|
|
void determineTargetColorSpace(const ImageInput& in, ImageSpec& target, ColorSpaceInfo& colorSpaceInfo);
|
|
|
|
void checkSpecsMatch(const ImageInput& current, const ImageSpec& firstSpec);
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
int CommandCreate::main(int argc, char* argv[]) {
|
|
try {
|
|
parseCommandLine("ktx create",
|
|
"Create, encode and supercompress a KTX2 file from the input images specified as the\n"
|
|
" input-file... arguments and save it as the output-file.",
|
|
argc, argv);
|
|
executeCreate();
|
|
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 CommandCreate::initOptions(cxxopts::Options& opts) {
|
|
options.init(opts);
|
|
}
|
|
|
|
void CommandCreate::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) {
|
|
options.process(opts, args, *this);
|
|
|
|
numLevels = options.levels.value_or(1);
|
|
numLayers = options.layers.value_or(1);
|
|
numFaces = options.cubemap ? 6 : 1;
|
|
baseDepth = options.depth.value_or(1u);
|
|
|
|
const auto blockSizeZ = isFormat3DBlockCompressed(options.vkFormat) ?
|
|
createFormatDescriptor(options.vkFormat, *this).basic.texelBlockDimension2 + 1u : 1u;
|
|
uint32_t expectedInputImages = 0;
|
|
for (uint32_t i = 0; i < (options.mipmapGenerate ? 1 : numLevels); ++i)
|
|
// If --generate-mipmap is set the input only contains the base level images
|
|
expectedInputImages += numLayers * numFaces * ceil_div(std::max(baseDepth >> i, 1u), blockSizeZ);
|
|
if (options.inputFilepaths.size() != expectedInputImages) {
|
|
fatal_usage("Too {} input image for {} level{}, {} layer, {} face and {} depth. Provided {} but expected {}.",
|
|
options.inputFilepaths.size() > expectedInputImages ? "many" : "few",
|
|
numLevels,
|
|
options.mipmapGenerate ? " (mips generated)" : "",
|
|
numLayers,
|
|
numFaces,
|
|
baseDepth,
|
|
options.inputFilepaths.size(), expectedInputImages);
|
|
}
|
|
|
|
if (!isFormatAstc(options.vkFormat)) {
|
|
for (const char* astcOption : OptionsASTC::kAstcOptions)
|
|
if (args[astcOption].count())
|
|
fatal_usage("--{} can only be used with ASTC formats.", astcOption);
|
|
}
|
|
|
|
if (options.codec == EncodeCodec::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.");
|
|
}
|
|
|
|
if (options.codec != EncodeCodec::NONE) {
|
|
switch (options.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(options.vkFormat)));
|
|
break;
|
|
}
|
|
}
|
|
|
|
const auto canCompare = options.codec == EncodeCodec::BasisLZ || options.codec == EncodeCodec::UASTC;
|
|
if (options.compare_ssim && !canCompare)
|
|
fatal_usage("--compare-ssim can only be used with BasisLZ or UASTC encoding.");
|
|
if (options.compare_psnr && !canCompare)
|
|
fatal_usage("--compare-psnr can only be used with BasisLZ or UASTC encoding.");
|
|
|
|
if (isFormatAstc(options.vkFormat) && !options.raw) {
|
|
options.encodeASTC = true;
|
|
|
|
switch (options.vkFormat) {
|
|
case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_4x4;
|
|
break;
|
|
case VK_FORMAT_ASTC_5x4_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_5x4;
|
|
break;
|
|
case VK_FORMAT_ASTC_5x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_5x5;
|
|
break;
|
|
case VK_FORMAT_ASTC_6x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_6x5;
|
|
break;
|
|
case VK_FORMAT_ASTC_6x6_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_6x6;
|
|
break;
|
|
case VK_FORMAT_ASTC_8x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_8x5;
|
|
break;
|
|
case VK_FORMAT_ASTC_8x6_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_8x6;
|
|
break;
|
|
case VK_FORMAT_ASTC_8x8_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_8x8;
|
|
break;
|
|
case VK_FORMAT_ASTC_10x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x5;
|
|
break;
|
|
case VK_FORMAT_ASTC_10x6_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x6;
|
|
break;
|
|
case VK_FORMAT_ASTC_10x8_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x8;
|
|
break;
|
|
case VK_FORMAT_ASTC_10x10_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_10x10;
|
|
break;
|
|
case VK_FORMAT_ASTC_12x10_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_12x10;
|
|
break;
|
|
case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
|
|
options.mode = KTX_PACK_ASTC_ENCODER_MODE_LDR;
|
|
options.blockDimension = KTX_PACK_ASTC_BLOCK_DIMENSION_12x12;
|
|
break;
|
|
default:
|
|
fatal(rc::NOT_SUPPORTED, "{} is unsupported for ASTC encoding.", toString(options.vkFormat));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (options._1d && options.encodeASTC)
|
|
fatal_usage("ASTC format {} cannot be used for 1 dimensional textures (indicated by --1d).",
|
|
toString(options.vkFormat));
|
|
}
|
|
|
|
template <typename F>
|
|
void CommandCreate::foreachImage(const FormatDescriptor& format, F&& func) {
|
|
// Input file ordering is specified as the same order as the
|
|
// "levelImages" structure in the KTX 2.0 specification:
|
|
// level > layer > face > image (z_slice_of_blocks)
|
|
// aka foreach level, foreach layer, foreach face, foreach image (z_slice_of_blocks)
|
|
|
|
auto inputFileIt = options.inputFilepaths.begin();
|
|
|
|
for (uint32_t levelIndex = 0; levelIndex < (options.mipmapGenerate ? 1 : numLevels); ++levelIndex) {
|
|
const auto numDepthSlices = ceil_div(std::max(baseDepth >> levelIndex, 1u), format.basic.texelBlockDimension2 + 1u);
|
|
for (uint32_t layerIndex = 0; layerIndex < numLayers; ++layerIndex) {
|
|
for (uint32_t faceIndex = 0; faceIndex < numFaces; ++faceIndex) {
|
|
for (uint32_t depthSliceIndex = 0; depthSliceIndex < numDepthSlices; ++depthSliceIndex) {
|
|
assert(inputFileIt != options.inputFilepaths.end() && "Internal error"); // inputFilepaths size was already validated during arg parsing
|
|
func(*inputFileIt++, levelIndex, layerIndex, faceIndex, depthSliceIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert(inputFileIt == options.inputFilepaths.end() && "Internal error"); // inputFilepaths size was already validated during arg parsing
|
|
}
|
|
|
|
std::string CommandCreate::readRawFile(const std::filesystem::path& filepath) {
|
|
std::string result;
|
|
InputStream inputStream(filepath.string(), *this);
|
|
|
|
inputStream->seekg(0, std::ios::end);
|
|
if (inputStream->fail())
|
|
fatal(rc::IO_FAILURE, "Failed to seek file \"{}\": {}.", filepath.generic_string(), errnoMessage());
|
|
|
|
const auto size = inputStream->tellg();
|
|
inputStream->seekg(0);
|
|
if (inputStream->fail())
|
|
fatal(rc::IO_FAILURE, "Failed to seek file \"{}\": {}.", filepath.generic_string(), errnoMessage());
|
|
|
|
result.resize(size);
|
|
inputStream->read(result.data(), size);
|
|
if (inputStream->fail())
|
|
fatal(rc::IO_FAILURE, "Failed to read file \"{}\": {}.", filepath.generic_string(), errnoMessage());
|
|
|
|
return result;
|
|
}
|
|
|
|
void CommandCreate::executeCreate() {
|
|
const auto warningFn = [this](const std::string& w) { this->warning(w); };
|
|
|
|
KTXTexture2 texture{nullptr};
|
|
targetChannelCount = options.formatDesc.channelCount();
|
|
|
|
ImageSpec target;
|
|
|
|
bool firstImage = true;
|
|
ImageSpec firstImageSpec{};
|
|
|
|
foreachImage(options.formatDesc, [&](
|
|
const auto& inputFilepath,
|
|
uint32_t levelIndex,
|
|
uint32_t layerIndex,
|
|
uint32_t faceIndex,
|
|
uint32_t depthSliceIndex) {
|
|
|
|
if (options.raw) {
|
|
if (std::exchange(firstImage, false)) {
|
|
target = ImageSpec{
|
|
options.width.value_or(1u),
|
|
options.height.value_or(1u),
|
|
options.depth.value_or(1u),
|
|
options.formatDesc};
|
|
|
|
if (options.cubemap && target.width() != target.height())
|
|
fatal(rc::INVALID_FILE, "--cubemap specified but the input image \"{}\" with size {}x{} is not square.",
|
|
fmtInFile(inputFilepath), target.width(), target.height());
|
|
|
|
if (options.assignOETF.has_value())
|
|
target.format().setTransfer(options.assignOETF.value());
|
|
|
|
if (options.assignPrimaries.has_value())
|
|
target.format().setPrimaries(options.assignPrimaries.value());
|
|
|
|
texture = createTexture(target);
|
|
}
|
|
|
|
const auto rawData = readRawFile(inputFilepath);
|
|
|
|
const auto expectedFileSize = ktxTexture_GetImageSize(texture, levelIndex);
|
|
if (rawData.size() != expectedFileSize)
|
|
fatal(rc::INVALID_FILE, "Raw input file \"{}\" with {} bytes for level {} does not match the expected size of {} bytes.",
|
|
fmtInFile(inputFilepath), rawData.size(), levelIndex, expectedFileSize);
|
|
|
|
const auto ret = ktxTexture_SetImageFromMemory(
|
|
texture,
|
|
levelIndex,
|
|
layerIndex,
|
|
faceIndex + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
|
|
reinterpret_cast<const ktx_uint8_t*>(rawData.data()),
|
|
rawData.size());
|
|
assert(ret == KTX_SUCCESS && "Internal error"); (void) ret;
|
|
} else {
|
|
const auto inputImageFile = ImageInput::open(inputFilepath, nullptr, warningFn);
|
|
inputImageFile->seekSubimage(0, 0); // Loading multiple subimage from the same input is not supported
|
|
|
|
target = ImageSpec{
|
|
inputImageFile->spec().width(),
|
|
inputImageFile->spec().height(),
|
|
options.depth.value_or(1u),
|
|
options.formatDesc};
|
|
|
|
ColorSpaceInfo colorSpaceInfo{};
|
|
determineTargetColorSpace(*inputImageFile, target, colorSpaceInfo);
|
|
|
|
if (std::exchange(firstImage, false)) {
|
|
if (options.cubemap && target.width() != target.height())
|
|
fatal(rc::INVALID_FILE, "--cubemap specified but the input image \"{}\" with size {}x{} is not square.",
|
|
fmtInFile(inputFilepath), target.width(), target.height());
|
|
|
|
if (options._1d && target.height() != 1)
|
|
fatal(rc::INVALID_FILE, "For --1d textures the input image height must be 1, but for \"{}\" it was {}.",
|
|
fmtInFile(inputFilepath), target.height());
|
|
|
|
const auto maxDimension = std::max(target.width(), std::max(target.height(), baseDepth));
|
|
const auto maxLevels = log2(maxDimension) + 1;
|
|
if (options.levels.value_or(1) > maxLevels)
|
|
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(), baseDepth, maxLevels);
|
|
|
|
if (options.encodeASTC)
|
|
selectASTCMode(inputImageFile->spec().format().largestChannelBitLength());
|
|
|
|
firstImageSpec = inputImageFile->spec();
|
|
texture = createTexture(target);
|
|
} else {
|
|
checkSpecsMatch(*inputImageFile, firstImageSpec);
|
|
}
|
|
|
|
const uint32_t imageWidth = std::max(firstImageSpec.width() >> levelIndex, 1u);
|
|
const uint32_t imageHeight = std::max(firstImageSpec.height() >> levelIndex, 1u);
|
|
|
|
if (inputImageFile->spec().width() != imageWidth || inputImageFile->spec().height() != imageHeight)
|
|
fatal(rc::INVALID_FILE, "Input image \"{}\" with size {}x{} does not match expected size {}x{} for level {}.",
|
|
fmtInFile(inputFilepath), inputImageFile->spec().width(), inputImageFile->spec().height(), imageWidth, imageHeight, levelIndex);
|
|
|
|
auto image = loadInputImage(*inputImageFile);
|
|
|
|
if (colorSpaceInfo.dstTransferFunction != nullptr) {
|
|
assert(colorSpaceInfo.srcTransferFunction != nullptr);
|
|
if (colorSpaceInfo.dstColorPrimaries != nullptr) {
|
|
assert(colorSpaceInfo.srcColorPrimaries != nullptr);
|
|
auto primaryTransform = colorSpaceInfo.srcColorPrimaries->transformTo(*colorSpaceInfo.dstColorPrimaries);
|
|
|
|
if (options.failOnColorConversions)
|
|
fatal(rc::INVALID_FILE,
|
|
"Input file \"{}\" would need color conversion as input and output primaries are different. "
|
|
"Use --assign-primaries and do not use --convert-primaries to avoid unwanted color conversions.",
|
|
fmtInFile(inputFilepath));
|
|
|
|
if (options.warnOnColorConversions)
|
|
warning("Input file \"{}\" is color converted as input and output primaries are different. "
|
|
"Use --assign-primaries and do not use --convert-primaries to avoid unwanted color conversions.",
|
|
fmtInFile(inputFilepath));
|
|
|
|
// Transform OETF with primary transform
|
|
image->transformColorSpace(*colorSpaceInfo.srcTransferFunction, *colorSpaceInfo.dstTransferFunction, &primaryTransform);
|
|
} else {
|
|
if (options.failOnColorConversions)
|
|
fatal(rc::INVALID_FILE,
|
|
"Input file \"{}\" would need color conversion as input and output transfer functions are different. "
|
|
"Use --assign-oetf and do not use --convert-oetf to avoid unwanted color conversions.",
|
|
fmtInFile(inputFilepath));
|
|
|
|
if (options.warnOnColorConversions)
|
|
warning("Input file \"{}\" is color converted as input and output transfer functions are different. "
|
|
"Use --assign-oetf and do not use --convert-oetf to avoid unwanted color conversions.",
|
|
fmtInFile(inputFilepath));
|
|
|
|
// Transform OETF without primary transform
|
|
image->transformColorSpace(*colorSpaceInfo.srcTransferFunction, *colorSpaceInfo.dstTransferFunction);
|
|
}
|
|
}
|
|
|
|
if (options.swizzleInput)
|
|
image->swizzle(*options.swizzleInput);
|
|
|
|
const auto imageData = convert(image, options.vkFormat, *inputImageFile);
|
|
|
|
const auto ret = ktxTexture_SetImageFromMemory(
|
|
texture,
|
|
levelIndex,
|
|
layerIndex,
|
|
faceIndex + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
|
|
imageData.data(),
|
|
imageData.size());
|
|
assert(ret == KTX_SUCCESS && "Internal error"); (void) ret;
|
|
|
|
if (options.mipmapGenerate) {
|
|
const auto maxDimension = std::max(target.width(), std::max(target.height(), baseDepth));
|
|
const auto maxLevels = log2(maxDimension) + 1;
|
|
uint32_t numMipLevels = options.levels.value_or(maxLevels);
|
|
generateMipLevels(texture, std::move(image), *inputImageFile, numMipLevels, layerIndex, faceIndex, depthSliceIndex);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add KTXwriter metadata
|
|
const auto writer = fmt::format("{} {}", commandName, version(options.testrun));
|
|
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
|
|
static_cast<uint32_t>(writer.size() + 1), // +1 to include the \0
|
|
writer.c_str());
|
|
|
|
// Add KTXswizzle metadata
|
|
if (options.swizzle) {
|
|
ktxHashList_AddKVPair(&texture->kvDataHead, KTX_SWIZZLE_KEY,
|
|
static_cast<uint32_t>(options.swizzle->size() + 1), // +1 to include the \0
|
|
options.swizzle->c_str());
|
|
}
|
|
|
|
// Encode and apply compression
|
|
encode(texture, options);
|
|
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<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);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
void CommandCreate::encode(KTXTexture2& texture, OptionsCodec<false>& opts) {
|
|
MetricsCalculator metrics;
|
|
metrics.saveReferenceImages(texture, options, *this);
|
|
|
|
if (opts.codec != EncodeCodec::NONE) {
|
|
auto ret = ktxTexture2_CompressBasisEx(texture, &opts.basisOpts);
|
|
if (ret != KTX_SUCCESS)
|
|
fatal(rc::KTX_FAILURE, "Failed to encode KTX2 file with codec \"{}\". KTX Error: {}",
|
|
to_underlying(opts.codec), ktxErrorString(ret));
|
|
}
|
|
|
|
metrics.decodeAndCalculateMetrics(texture, options, *this);
|
|
}
|
|
|
|
void CommandCreate::encodeASTC(KTXTexture2& texture, OptionsASTC& opts) {
|
|
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));
|
|
}
|
|
}
|
|
|
|
void CommandCreate::compress(KTXTexture2& texture, const OptionsCompress& opts) {
|
|
if (opts.zstd) {
|
|
const auto ret = ktxTexture2_DeflateZstd(texture, *opts.zstd);
|
|
if (ret != KTX_SUCCESS)
|
|
fatal(rc::KTX_FAILURE, "Zstd deflation failed. KTX Error: {}", ktxErrorString(ret));
|
|
}
|
|
|
|
if (opts.zlib) {
|
|
const auto ret = ktxTexture2_DeflateZLIB(texture, *opts.zlib);
|
|
if (ret != KTX_SUCCESS)
|
|
fatal(rc::KTX_FAILURE, "ZLIB deflation failed. KTX Error: {}", ktxErrorString(ret));
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
std::unique_ptr<Image> CommandCreate::loadInputImage(ImageInput& inputImageFile) {
|
|
std::unique_ptr<Image> image = nullptr;
|
|
|
|
const auto& inputFormat = inputImageFile.spec().format();
|
|
const auto width = inputImageFile.spec().width();
|
|
const auto height = inputImageFile.spec().height();
|
|
|
|
const auto inputBitLength = inputFormat.largestChannelBitLength();
|
|
const auto requestBitLength = std::max(imageio::bit_ceil(inputBitLength), 8u);
|
|
FormatDescriptor loadFormat;
|
|
|
|
switch (inputImageFile.formatType()) {
|
|
case ImageInputFormatType::exr_uint:
|
|
image = std::make_unique<rgba32image>(width, height);
|
|
loadFormat = createFormatDescriptor(VK_FORMAT_R32G32B32A32_UINT, *this);
|
|
break;
|
|
|
|
case ImageInputFormatType::exr_float:
|
|
image = std::make_unique<rgba32fimage>(width, height);
|
|
loadFormat = createFormatDescriptor(VK_FORMAT_R32G32B32A32_SFLOAT, *this);
|
|
break;
|
|
|
|
case ImageInputFormatType::npbm: [[fallthrough]];
|
|
case ImageInputFormatType::jpg: [[fallthrough]];
|
|
case ImageInputFormatType::png_l: [[fallthrough]];
|
|
case ImageInputFormatType::png_la: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgb: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgba:
|
|
if (requestBitLength == 8) {
|
|
image = std::make_unique<rgba8image>(width, height);
|
|
loadFormat = createFormatDescriptor(VK_FORMAT_R8G8B8A8_UNORM, *this);
|
|
break;
|
|
} else if (requestBitLength == 16) {
|
|
image = std::make_unique<rgba16image>(width, height);
|
|
loadFormat = createFormatDescriptor(VK_FORMAT_R16G16B16A16_UNORM, *this);
|
|
break;
|
|
} else {
|
|
fatal(rc::INVALID_FILE, "Unsupported format with {}-bit channels.", requestBitLength);
|
|
}
|
|
break;
|
|
}
|
|
|
|
inputImageFile.readImage(static_cast<uint8_t*>(*image), image->getByteCount(), 0, 0, loadFormat);
|
|
return image;
|
|
}
|
|
|
|
std::vector<uint8_t> convertUNORMPacked(const std::unique_ptr<Image>& image, uint32_t C0, uint32_t C1, uint32_t C2, uint32_t C3, std::string_view swizzle = "") {
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getUNORMPacked(C0, C1, C2, C3);
|
|
}
|
|
|
|
template <typename T>
|
|
std::vector<uint8_t> convertUNORM(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
|
|
using ComponentT = typename T::Color::value_type;
|
|
static constexpr auto componentCount = T::Color::getComponentCount();
|
|
static constexpr auto bytesPerComponent = sizeof(ComponentT);
|
|
static constexpr auto bits = bytesPerComponent * 8;
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getUNORM(componentCount, bits);
|
|
}
|
|
|
|
template <typename T>
|
|
std::vector<uint8_t> convertUNORMSBits(const std::unique_ptr<Image>& image, uint32_t sBits, std::string_view swizzle = "") {
|
|
using ComponentT = typename T::Color::value_type;
|
|
static constexpr auto componentCount = T::Color::getComponentCount();
|
|
static constexpr auto bytesPerComponent = sizeof(ComponentT);
|
|
static constexpr auto bits = bytesPerComponent * 8;
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getUNORM(componentCount, bits, sBits);
|
|
}
|
|
|
|
template <typename T>
|
|
std::vector<uint8_t> convertSFLOAT(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
|
|
using ComponentT = typename T::Color::value_type;
|
|
static constexpr auto componentCount = T::Color::getComponentCount();
|
|
static constexpr auto bytesPerComponent = sizeof(ComponentT);
|
|
static constexpr auto bits = bytesPerComponent * 8;
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getSFloat(componentCount, bits);
|
|
}
|
|
|
|
std::vector<uint8_t> convertB10G11R11(const std::unique_ptr<Image>& image) {
|
|
return image->getB10G11R11();
|
|
}
|
|
|
|
std::vector<uint8_t> convertE5B9G9R9(const std::unique_ptr<Image>& image) {
|
|
return image->getE5B9G9R9();
|
|
}
|
|
|
|
template <typename T>
|
|
std::vector<uint8_t> convertUINT(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
|
|
using ComponentT = typename T::Color::value_type;
|
|
static constexpr auto componentCount = T::Color::getComponentCount();
|
|
static constexpr auto bytesPerComponent = sizeof(ComponentT);
|
|
static constexpr auto bits = bytesPerComponent * 8;
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getUINT(componentCount, bits);
|
|
}
|
|
|
|
std::vector<uint8_t> convertUINTPacked(const std::unique_ptr<Image>& image,
|
|
uint32_t c0 = 0, uint32_t c1 = 0, uint32_t c2 = 0, uint32_t c3 = 0,
|
|
std::string_view swizzle = "") {
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getUINTPacked(c0, c1, c2, c3);
|
|
}
|
|
|
|
std::vector<uint8_t> convertSINTPacked(const std::unique_ptr<Image>& image,
|
|
uint32_t c0 = 0, uint32_t c1 = 0, uint32_t c2 = 0, uint32_t c3 = 0,
|
|
std::string_view swizzle = "") {
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getSINTPacked(c0, c1, c2, c3);
|
|
}
|
|
|
|
template <typename T>
|
|
std::vector<uint8_t> convertSINT(const std::unique_ptr<Image>& image, std::string_view swizzle = "") {
|
|
using ComponentT = typename T::Color::value_type;
|
|
static constexpr auto componentCount = T::Color::getComponentCount();
|
|
static constexpr auto bytesPerComponent = sizeof(ComponentT);
|
|
static constexpr auto bits = bytesPerComponent * 8;
|
|
|
|
if (!swizzle.empty())
|
|
image->swizzle(swizzle);
|
|
|
|
return image->getSINT(componentCount, bits);
|
|
}
|
|
|
|
std::vector<uint8_t> CommandCreate::convert(const std::unique_ptr<Image>& image, VkFormat vkFormat,
|
|
ImageInput& inputFile) {
|
|
|
|
const uint32_t inputBitDepth = std::max(8u, inputFile.spec().format().largestChannelBitLength());
|
|
|
|
const auto require = [&](uint32_t bitDepth) {
|
|
if (inputBitDepth < bitDepth)
|
|
fatal(rc::INVALID_FILE, "{}: Not enough precision to convert {} bit input to {} bit output for {}.",
|
|
inputFile.filename(), inputBitDepth, bitDepth, toString(vkFormat));
|
|
if (inputBitDepth > imageio::bit_ceil(bitDepth))
|
|
warning("{}: Possible loss of precision with converting {} bit input to {} bit output for {}.",
|
|
inputFile.filename(), inputBitDepth, bitDepth, toString(vkFormat));
|
|
};
|
|
const auto requireUNORM = [&](uint32_t bitDepth) {
|
|
switch (inputFile.formatType()) {
|
|
case ImageInputFormatType::png_l: [[fallthrough]];
|
|
case ImageInputFormatType::png_la: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgb: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgba: [[fallthrough]];
|
|
case ImageInputFormatType::npbm: [[fallthrough]];
|
|
case ImageInputFormatType::jpg:
|
|
break; // Accept
|
|
case ImageInputFormatType::exr_uint: [[fallthrough]];
|
|
case ImageInputFormatType::exr_float:
|
|
fatal(rc::INVALID_FILE, "{}: Input file data type \"{}\" does not match the expected input data type of {} bit \"{}\" for {}.",
|
|
inputFile.filename(), toString(inputFile.formatType()), bitDepth, "UNORM", toString(vkFormat));
|
|
}
|
|
require(bitDepth);
|
|
};
|
|
const auto requireSFloat = [&](uint32_t bitDepth) {
|
|
switch (inputFile.formatType()) {
|
|
case ImageInputFormatType::exr_float:
|
|
break; // Accept
|
|
case ImageInputFormatType::png_l: [[fallthrough]];
|
|
case ImageInputFormatType::png_la: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgb: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgba: [[fallthrough]];
|
|
case ImageInputFormatType::npbm: [[fallthrough]];
|
|
case ImageInputFormatType::jpg: [[fallthrough]];
|
|
case ImageInputFormatType::exr_uint:
|
|
fatal(rc::INVALID_FILE, "{}: Input file data type \"{}\" does not match the expected input data type of {} bit \"{}\" for {}.",
|
|
inputFile.filename(), toString(inputFile.formatType()), bitDepth, "SFLOAT", toString(vkFormat));
|
|
}
|
|
require(bitDepth);
|
|
};
|
|
const auto requireUINT = [&](uint32_t bitDepth) {
|
|
switch (inputFile.formatType()) {
|
|
case ImageInputFormatType::exr_uint:
|
|
break; // Accept
|
|
case ImageInputFormatType::png_l: [[fallthrough]];
|
|
case ImageInputFormatType::png_la: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgb: [[fallthrough]];
|
|
case ImageInputFormatType::png_rgba: [[fallthrough]];
|
|
case ImageInputFormatType::npbm: [[fallthrough]];
|
|
case ImageInputFormatType::jpg: [[fallthrough]];
|
|
case ImageInputFormatType::exr_float:
|
|
fatal(rc::INVALID_FILE, "{}: Input file data type \"{}\" does not match the expected input data type of {} bit \"{}\" for {}.",
|
|
inputFile.filename(), toString(inputFile.formatType()), bitDepth, "UINT", toString(vkFormat));
|
|
}
|
|
require(bitDepth);
|
|
};
|
|
|
|
// ------------
|
|
|
|
switch (vkFormat) {
|
|
// PNG:
|
|
|
|
case VK_FORMAT_R8_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_R8_SRGB:
|
|
requireUNORM(8);
|
|
return convertUNORM<r8image>(image);
|
|
case VK_FORMAT_R8G8_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_R8G8_SRGB:
|
|
requireUNORM(8);
|
|
return convertUNORM<rg8image>(image);
|
|
case VK_FORMAT_R8G8B8_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_R8G8B8_SRGB:
|
|
requireUNORM(8);
|
|
return convertUNORM<rgb8image>(image);
|
|
case VK_FORMAT_B8G8R8_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_B8G8R8_SRGB:
|
|
requireUNORM(8);
|
|
return convertUNORM<rgb8image>(image, "bgr1");
|
|
|
|
// Verbatim copy with component reordering if needed, extra channels must be dropped.
|
|
//
|
|
// Input files that have 16-bit components must be truncated to
|
|
// 8 bits with a right-shift and a warning must be generated in the stderr.
|
|
|
|
case VK_FORMAT_R8G8B8A8_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_R8G8B8A8_SRGB: [[fallthrough]];
|
|
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: [[fallthrough]];
|
|
case VK_FORMAT_A8B8G8R8_SRGB_PACK32:
|
|
requireUNORM(8);
|
|
return convertUNORM<rgba8image>(image);
|
|
case VK_FORMAT_B8G8R8A8_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_B8G8R8A8_SRGB:
|
|
requireUNORM(8);
|
|
return convertUNORM<rgba8image>(image, "bgra");
|
|
|
|
// Verbatim copy with component reordering if needed, extra channels must be dropped.
|
|
|
|
// Input files that have 16-bit components must be truncated to
|
|
// 8 bits with a right-shift and a warning must be generated in the stderr.
|
|
|
|
case VK_FORMAT_ASTC_4x4_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_5x4_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_5x4_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_5x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_5x5_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_6x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_6x5_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_6x6_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_6x6_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x5_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x6_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x6_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x8_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_8x8_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x5_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x5_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x6_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x6_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x8_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x8_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x10_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_10x10_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_12x10_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_12x10_SRGB_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_12x12_UNORM_BLOCK: [[fallthrough]];
|
|
case VK_FORMAT_ASTC_12x12_SRGB_BLOCK:
|
|
// ASTC texture data composition is performed via
|
|
// R8G8B8A8_UNORM followed by the ASTC encoding
|
|
requireUNORM(8);
|
|
assert(false && "Internal error");
|
|
return {};
|
|
|
|
// Passthrough CLI options to the ASTC encoder.
|
|
|
|
case VK_FORMAT_R4G4_UNORM_PACK8:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 4, 4, 0, 0);
|
|
case VK_FORMAT_R5G6B5_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 5, 6, 5, 0);
|
|
case VK_FORMAT_B5G6R5_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 5, 6, 5, 0, "bgr1");
|
|
|
|
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 4, 4, 4, 4);
|
|
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 4, 4, 4, 4, "bgra");
|
|
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 5, 5, 5, 1);
|
|
case VK_FORMAT_B5G5R5A1_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 5, 5, 5, 1, "bgra");
|
|
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 1, 5, 5, 5, "argb");
|
|
case VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 1, 5, 5, 5, "abgr");
|
|
case VK_FORMAT_A4R4G4B4_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 4, 4, 4, 4, "argb");
|
|
case VK_FORMAT_A4B4G4R4_UNORM_PACK16:
|
|
requireUNORM(8);
|
|
return convertUNORMPacked(image, 4, 4, 4, 4, "abgr");
|
|
|
|
// Input values must be rounded to the target precision.
|
|
// When the input file contains an sBIT chunk, its values must be taken into account.
|
|
|
|
case VK_FORMAT_R10X6_UNORM_PACK16:
|
|
requireUNORM(10);
|
|
return convertUNORMSBits<r16image>(image, 10);
|
|
case VK_FORMAT_R10X6G10X6_UNORM_2PACK16:
|
|
requireUNORM(10);
|
|
return convertUNORMSBits<rg16image>(image, 10);
|
|
case VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16:
|
|
requireUNORM(10);
|
|
return convertUNORMSBits<rgba16image>(image, 10);
|
|
|
|
case VK_FORMAT_R12X4_UNORM_PACK16:
|
|
requireUNORM(12);
|
|
return convertUNORMSBits<r16image>(image, 12);
|
|
case VK_FORMAT_R12X4G12X4_UNORM_2PACK16:
|
|
requireUNORM(12);
|
|
return convertUNORMSBits<rg16image>(image, 12);
|
|
case VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16:
|
|
requireUNORM(12);
|
|
return convertUNORMSBits<rgba16image>(image, 12);
|
|
|
|
// Input values must be rounded to the target precision.
|
|
// When the input file contains an sBIT chunk, its values must be taken into account.
|
|
|
|
case VK_FORMAT_R16_UNORM:
|
|
requireUNORM(16);
|
|
return convertUNORM<r16image>(image);
|
|
case VK_FORMAT_R16G16_UNORM:
|
|
requireUNORM(16);
|
|
return convertUNORM<rg16image>(image);
|
|
case VK_FORMAT_R16G16B16_UNORM:
|
|
requireUNORM(16);
|
|
return convertUNORM<rgb16image>(image);
|
|
case VK_FORMAT_R16G16B16A16_UNORM:
|
|
requireUNORM(16);
|
|
return convertUNORM<rgba16image>(image);
|
|
|
|
// Verbatim copy, extra channels must be dropped.
|
|
// Input PNG file must be 16-bit with sBIT chunk missing or signaling 16 bits.
|
|
|
|
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
|
|
requireUNORM(10);
|
|
return convertUNORMPacked(image, 2, 10, 10, 10, "argb");
|
|
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
|
requireUNORM(10);
|
|
return convertUNORMPacked(image, 2, 10, 10, 10, "abgr");
|
|
|
|
// Input values must be rounded to the target precision.
|
|
// When the input file contains an sBIT chunk, its values must be taken into account.
|
|
|
|
case VK_FORMAT_G8B8G8R8_422_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_B8G8R8G8_422_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16: [[fallthrough]];
|
|
case VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16: [[fallthrough]];
|
|
case VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16: [[fallthrough]];
|
|
case VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16: [[fallthrough]];
|
|
case VK_FORMAT_G16B16G16R16_422_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_B16G16R16G16_422_UNORM:
|
|
fatal(rc::INVALID_ARGUMENTS, "Unsupported format for non-raw create: {}.", toString(options.vkFormat));
|
|
break;
|
|
|
|
// EXR:
|
|
|
|
case VK_FORMAT_R8_UINT:
|
|
requireSFloat(16);
|
|
return convertUINT<r8image>(image);
|
|
case VK_FORMAT_R8_SINT:
|
|
requireSFloat(16);
|
|
return convertSINT<r8image>(image);
|
|
case VK_FORMAT_R16_UINT:
|
|
requireSFloat(32);
|
|
return convertUINT<r16image>(image);
|
|
case VK_FORMAT_R16_SINT:
|
|
requireSFloat(32);
|
|
return convertSINT<r16image>(image);
|
|
case VK_FORMAT_R32_UINT:
|
|
requireUINT(32);
|
|
return convertUINT<r32image>(image);
|
|
case VK_FORMAT_R8G8_UINT:
|
|
requireSFloat(16);
|
|
return convertUINT<rg8image>(image);
|
|
case VK_FORMAT_R8G8_SINT:
|
|
requireSFloat(16);
|
|
return convertSINT<rg8image>(image);
|
|
case VK_FORMAT_R16G16_UINT:
|
|
requireSFloat(32);
|
|
return convertUINT<rg16image>(image);
|
|
case VK_FORMAT_R16G16_SINT:
|
|
requireSFloat(32);
|
|
return convertSINT<rg16image>(image);
|
|
case VK_FORMAT_R32G32_UINT:
|
|
requireUINT(32);
|
|
return convertUINT<rg32image>(image);
|
|
case VK_FORMAT_R8G8B8_UINT:
|
|
requireSFloat(16);
|
|
return convertUINT<rgb8image>(image);
|
|
case VK_FORMAT_R8G8B8_SINT:
|
|
requireSFloat(16);
|
|
return convertSINT<rgb8image>(image);
|
|
case VK_FORMAT_B8G8R8_UINT:
|
|
requireSFloat(16);
|
|
return convertUINT<rgb8image>(image, "bgr1");
|
|
case VK_FORMAT_B8G8R8_SINT:
|
|
requireSFloat(16);
|
|
return convertSINT<rgb8image>(image, "bgr1");
|
|
case VK_FORMAT_R16G16B16_UINT:
|
|
requireSFloat(32);
|
|
return convertUINT<rgb16image>(image);
|
|
case VK_FORMAT_R16G16B16_SINT:
|
|
requireSFloat(32);
|
|
return convertSINT<rgb16image>(image);
|
|
case VK_FORMAT_R32G32B32_UINT:
|
|
requireUINT(32);
|
|
return convertUINT<rgb32image>(image);
|
|
case VK_FORMAT_R8G8B8A8_UINT: [[fallthrough]];
|
|
case VK_FORMAT_A8B8G8R8_UINT_PACK32:
|
|
requireSFloat(16);
|
|
return convertUINT<rgba8image>(image);
|
|
case VK_FORMAT_R8G8B8A8_SINT: [[fallthrough]];
|
|
case VK_FORMAT_A8B8G8R8_SINT_PACK32:
|
|
requireSFloat(16);
|
|
return convertSINT<rgba8image>(image);
|
|
case VK_FORMAT_B8G8R8A8_UINT:
|
|
requireSFloat(16);
|
|
return convertUINT<rgba8image>(image, "bgra");
|
|
case VK_FORMAT_B8G8R8A8_SINT:
|
|
requireSFloat(16);
|
|
return convertSINT<rgba8image>(image, "bgra");
|
|
case VK_FORMAT_R16G16B16A16_UINT:
|
|
requireSFloat(32);
|
|
return convertUINT<rgba16image>(image);
|
|
case VK_FORMAT_R16G16B16A16_SINT:
|
|
requireSFloat(32);
|
|
return convertSINT<rgba16image>(image);
|
|
case VK_FORMAT_R32G32B32A32_UINT:
|
|
requireUINT(32);
|
|
return convertUINT<rgba32image>(image);
|
|
|
|
case VK_FORMAT_A2R10G10B10_UINT_PACK32:
|
|
requireSFloat(16);
|
|
return convertUINTPacked(image, 2, 10, 10, 10, "argb");
|
|
case VK_FORMAT_A2R10G10B10_SINT_PACK32:
|
|
requireSFloat(16);
|
|
return convertSINTPacked(image, 2, 10, 10, 10, "argb");
|
|
case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
|
requireSFloat(16);
|
|
return convertUINTPacked(image, 2, 10, 10, 10, "abgr");
|
|
case VK_FORMAT_A2B10G10R10_SINT_PACK32:
|
|
requireSFloat(16);
|
|
return convertSINTPacked(image, 2, 10, 10, 10, "abgr");
|
|
|
|
// The same EXR pixel types as for the decoding must be enforced.
|
|
// Extra channels must be dropped.
|
|
|
|
case VK_FORMAT_R16_SFLOAT:
|
|
requireSFloat(16);
|
|
return convertSFLOAT<r16image>(image);
|
|
case VK_FORMAT_R16G16_SFLOAT:
|
|
requireSFloat(16);
|
|
return convertSFLOAT<rg16image>(image);
|
|
case VK_FORMAT_R16G16B16_SFLOAT:
|
|
requireSFloat(16);
|
|
return convertSFLOAT<rgb16image>(image);
|
|
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
|
requireSFloat(16);
|
|
return convertSFLOAT<rgba16image>(image);
|
|
|
|
case VK_FORMAT_R32_SFLOAT:
|
|
requireSFloat(32);
|
|
return convertSFLOAT<r32image>(image);
|
|
case VK_FORMAT_R32G32_SFLOAT:
|
|
requireSFloat(32);
|
|
return convertSFLOAT<rg32image>(image);
|
|
case VK_FORMAT_R32G32B32_SFLOAT:
|
|
requireSFloat(32);
|
|
return convertSFLOAT<rgb32image>(image);
|
|
case VK_FORMAT_R32G32B32A32_SFLOAT:
|
|
requireSFloat(32);
|
|
return convertSFLOAT<rgba32image>(image);
|
|
|
|
// The same EXR pixel types as for the decoding must be enforced.
|
|
// Extra channels must be dropped.
|
|
|
|
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
|
|
requireSFloat(16);
|
|
return convertB10G11R11(image);
|
|
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
|
|
requireSFloat(16);
|
|
return convertE5B9G9R9(image);
|
|
|
|
// Input data must be rounded to the target precision.
|
|
|
|
case VK_FORMAT_D16_UNORM: [[fallthrough]];
|
|
case VK_FORMAT_X8_D24_UNORM_PACK32: [[fallthrough]];
|
|
case VK_FORMAT_D32_SFLOAT: [[fallthrough]];
|
|
case VK_FORMAT_S8_UINT: [[fallthrough]];
|
|
case VK_FORMAT_D16_UNORM_S8_UINT: [[fallthrough]];
|
|
case VK_FORMAT_D24_UNORM_S8_UINT: [[fallthrough]];
|
|
case VK_FORMAT_D32_SFLOAT_S8_UINT:
|
|
fatal(rc::INVALID_ARGUMENTS, "Unsupported format for non-raw create: {}.", toString(options.vkFormat));
|
|
break;
|
|
|
|
case VK_FORMAT_A8_UNORM_KHR:
|
|
// Special case for alpha-only
|
|
requireUNORM(8);
|
|
return convertUNORM<r8image>(image, "a000");
|
|
break;
|
|
|
|
// Not supported
|
|
|
|
default:
|
|
fatal(rc::INVALID_ARGUMENTS, "Requested format conversion is not yet implemented for: {}.", toString(options.vkFormat));
|
|
}
|
|
|
|
assert(false && "Internal error");
|
|
return {};
|
|
}
|
|
|
|
KTXTexture2 CommandCreate::createTexture(const ImageSpec& target) {
|
|
ktxTextureCreateInfo createInfo;
|
|
std::memset(&createInfo, 0, sizeof(createInfo));
|
|
|
|
assert(target.depth() == baseDepth);
|
|
|
|
createInfo.vkFormat = options.vkFormat;
|
|
createInfo.numFaces = numFaces;
|
|
createInfo.numLayers = numLayers;
|
|
createInfo.isArray = options.layers > 0u;
|
|
createInfo.baseWidth = target.width();
|
|
createInfo.baseHeight = target.height();
|
|
createInfo.baseDepth = target.depth();
|
|
createInfo.numDimensions = options._1d ? 1 : (options.depth > 0u ? 3 : 2);
|
|
|
|
if (options.mipmapRuntime) {
|
|
createInfo.generateMipmaps = true;
|
|
createInfo.numLevels = 1;
|
|
} else {
|
|
createInfo.generateMipmaps = false;
|
|
if (options.mipmapGenerate) {
|
|
const auto maxDimension = std::max(target.width(), std::max(target.height(), target.depth()));
|
|
const auto maxLevels = log2(maxDimension) + 1;
|
|
createInfo.numLevels = options.levels.value_or(maxLevels);
|
|
} else {
|
|
createInfo.numLevels = numLevels;
|
|
}
|
|
}
|
|
|
|
KTXTexture2 texture{nullptr};
|
|
ktx_error_code_e ret = ktxTexture2_Create(&createInfo, KTX_TEXTURE_CREATE_ALLOC_STORAGE, texture.pHandle());
|
|
if (KTX_SUCCESS != ret)
|
|
fatal(rc::KTX_FAILURE, "Failed to create ktxTexture: libktx error: {}", ktxErrorString(ret));
|
|
|
|
KHR_DFDSETVAL(texture->pDfd + 1, PRIMARIES, target.format().primaries());
|
|
KHR_DFDSETVAL(texture->pDfd + 1, TRANSFER, target.format().transfer());
|
|
|
|
return texture;
|
|
}
|
|
|
|
void CommandCreate::generateMipLevels(KTXTexture2& texture, std::unique_ptr<Image> image, ImageInput& inputFile,
|
|
uint32_t numMipLevels, uint32_t layerIndex, uint32_t faceIndex, uint32_t depthSliceIndex) {
|
|
|
|
if (isFormatINT(static_cast<VkFormat>(texture->vkFormat)))
|
|
fatal(rc::NOT_SUPPORTED, "Mipmap generation for SINT or UINT format {} is not supported.", toString(static_cast<VkFormat>(texture->vkFormat)));
|
|
|
|
const auto baseWidth = image->getWidth();
|
|
const auto baseHeight = image->getHeight();
|
|
|
|
for (uint32_t mipLevelIndex = 1; mipLevelIndex < numMipLevels; ++mipLevelIndex) {
|
|
const auto mipImageWidth = std::max(1u, baseWidth >> (mipLevelIndex));
|
|
const auto mipImageHeight = std::max(1u, baseHeight >> (mipLevelIndex));
|
|
|
|
try {
|
|
image = image->resample(mipImageWidth, mipImageHeight,
|
|
options.mipmapFilter.value_or(options.defaultMipmapFilter).c_str(),
|
|
options.mipmapFilterScale.value_or(options.defaultMipmapFilterScale),
|
|
options.mipmapWrap.value_or(options.defaultMipmapWrap));
|
|
} catch (const std::exception& e) {
|
|
fatal(rc::RUNTIME_ERROR, "Mipmap generation failed: {}", e.what());
|
|
}
|
|
|
|
const auto imageData = convert(image, options.vkFormat, inputFile);
|
|
|
|
const auto ret = ktxTexture_SetImageFromMemory(
|
|
texture,
|
|
mipLevelIndex,
|
|
layerIndex,
|
|
faceIndex + depthSliceIndex, // Faces and Depths are mutually exclusive, Addition is acceptable
|
|
imageData.data(),
|
|
imageData.size());
|
|
assert(ret == KTX_SUCCESS && "Internal error"); (void) ret;
|
|
}
|
|
}
|
|
|
|
void CommandCreate::selectASTCMode(uint32_t bitLength) {
|
|
if (options.mode == KTX_PACK_ASTC_ENCODER_MODE_DEFAULT) {
|
|
// If no astc mode option is specified and if input is <= 8bit
|
|
// default to LDR otherwise default to HDR
|
|
options.mode = bitLength <= 8 ? KTX_PACK_ASTC_ENCODER_MODE_LDR : KTX_PACK_ASTC_ENCODER_MODE_HDR;
|
|
} else {
|
|
if (bitLength > 8 && options.mode == KTX_PACK_ASTC_ENCODER_MODE_LDR)
|
|
// Input is > 8-bit and user wants LDR, issue quality loss warning.
|
|
warning("Input file is 16-bit but ASTC LDR option is specified. Expect quality loss in the output.");
|
|
else if (bitLength < 16 && options.mode == KTX_PACK_ASTC_ENCODER_MODE_HDR)
|
|
// Input is < 16-bit and user wants HDR, issue warning.
|
|
warning("Input file is not 16-bit but HDR option is specified.");
|
|
}
|
|
|
|
// ASTC Encoding is performed by first creating a RGBA8 texture then encode it afterward
|
|
|
|
// Encode based on non-8-bit input (aka true HDR) is currently not supported by
|
|
// ktxTexture2_CompressAstcEx. Once supported suitable formats can be chosen here
|
|
if (isFormatSRGB(options.vkFormat))
|
|
options.vkFormat = VK_FORMAT_R8G8B8A8_SRGB;
|
|
else
|
|
options.vkFormat = VK_FORMAT_R8G8B8A8_UNORM;
|
|
}
|
|
|
|
std::unique_ptr<const ColorPrimaries> CommandCreate::createColorPrimaries(khr_df_primaries_e primaries) const {
|
|
switch (primaries) {
|
|
case KHR_DF_PRIMARIES_BT709:
|
|
return std::make_unique<ColorPrimariesBT709>();
|
|
case KHR_DF_PRIMARIES_BT601_EBU:
|
|
return std::make_unique<ColorPrimariesBT601_625_EBU>();
|
|
case KHR_DF_PRIMARIES_BT601_SMPTE:
|
|
return std::make_unique<ColorPrimariesBT601_525_SMPTE>();
|
|
case KHR_DF_PRIMARIES_BT2020:
|
|
return std::make_unique<ColorPrimariesBT2020>();
|
|
case KHR_DF_PRIMARIES_CIEXYZ:
|
|
return std::make_unique<ColorPrimariesCIEXYZ>();
|
|
case KHR_DF_PRIMARIES_ACES:
|
|
return std::make_unique<ColorPrimariesACES>();
|
|
case KHR_DF_PRIMARIES_ACESCC:
|
|
return std::make_unique<ColorPrimariesACEScc>();
|
|
case KHR_DF_PRIMARIES_NTSC1953:
|
|
return std::make_unique<ColorPrimariesNTSC1953>();
|
|
case KHR_DF_PRIMARIES_PAL525:
|
|
return std::make_unique<ColorPrimariesPAL525>();
|
|
case KHR_DF_PRIMARIES_DISPLAYP3:
|
|
return std::make_unique<ColorPrimariesDisplayP3>();
|
|
case KHR_DF_PRIMARIES_ADOBERGB:
|
|
return std::make_unique<ColorPrimariesAdobeRGB>();
|
|
default:
|
|
assert(false);
|
|
// We return BT709 by default if some error happened
|
|
return std::make_unique<ColorPrimariesBT709>();
|
|
}
|
|
}
|
|
|
|
void CommandCreate::determineTargetColorSpace(const ImageInput& in, ImageSpec& target, ColorSpaceInfo& colorSpaceInfo) {
|
|
// Primaries handling:
|
|
//
|
|
// 1. Use assign-primaries option value, if set.
|
|
// 2. Use primaries info given by plugin.
|
|
// 3. If no primaries info and input is PNG use PNG spec.
|
|
// recommendation of BT709/sRGB otherwise leave as
|
|
// UNSPECIFIED.
|
|
// 4. If convert-primaries is specified but no primaries info is
|
|
// given by the plugin then fail.
|
|
// 5. If convert-primaries is specified and primaries info determined
|
|
// above is different then set up conversion.
|
|
const ImageSpec& spec = in.spec();
|
|
|
|
// Set Primaries
|
|
colorSpaceInfo.usedInputPrimaries = spec.format().primaries();
|
|
if (options.assignPrimaries.has_value()) {
|
|
colorSpaceInfo.usedInputPrimaries = options.assignPrimaries.value();
|
|
target.format().setPrimaries(options.assignPrimaries.value());
|
|
} else if (spec.format().primaries() != KHR_DF_PRIMARIES_UNSPECIFIED) {
|
|
target.format().setPrimaries(spec.format().primaries());
|
|
} else {
|
|
if (!in.formatName().compare("png")) {
|
|
warning("No color primaries in PNG input file \"{}\", defaulting to BT.709.", in.filename());
|
|
colorSpaceInfo.usedInputPrimaries = KHR_DF_PRIMARIES_BT709;
|
|
target.format().setPrimaries(KHR_DF_PRIMARIES_BT709);
|
|
} else {
|
|
// Leave as unspecified.
|
|
target.format().setPrimaries(spec.format().primaries());
|
|
}
|
|
}
|
|
|
|
if (options.convertPrimaries.has_value()) {
|
|
if (colorSpaceInfo.usedInputPrimaries == KHR_DF_PRIMARIES_UNSPECIFIED) {
|
|
fatal(rc::INVALID_FILE, "Cannot convert primaries as no information about the color primaries "
|
|
"is available in the input file \"{}\". Use --assign-primaries to specify one.", in.filename());
|
|
} else if (options.convertPrimaries.value() != colorSpaceInfo.usedInputPrimaries) {
|
|
colorSpaceInfo.srcColorPrimaries = createColorPrimaries(colorSpaceInfo.usedInputPrimaries);
|
|
colorSpaceInfo.dstColorPrimaries = createColorPrimaries(options.convertPrimaries.value());
|
|
target.format().setPrimaries(options.convertPrimaries.value());
|
|
}
|
|
}
|
|
|
|
// OETF / Transfer function handling in priority order:
|
|
//
|
|
// 1. Use assign-oetf option value, if set.
|
|
// 2. Use OETF signalled by plugin as the input transfer function if
|
|
// linear, sRGB, ITU, or PQ EOTF. For all others, throw error.
|
|
// 3. If ICC profile signalled, throw error. Known ICC profiles are
|
|
// handled by the plugin.
|
|
// 4. If gamma of 1.0 signalled assume linear input transfer function.
|
|
// If gamma of .45454 signalled, set up for conversion from gamma
|
|
// and warn user about the conversion.
|
|
// If gamma of 0.0 is signalled, for PNG follow W3C recommendation
|
|
// per step 5. For any other gamma value, just convert it.
|
|
// 5. If no color info is signalled, and input is PNG follow W3C
|
|
// recommendation of sRGB for 8-bit, linear otherwise. For other input
|
|
// formats throw error.
|
|
// 6. Convert OETF based on convert-oetf option value or as described
|
|
// above.
|
|
|
|
colorSpaceInfo.usedInputTransferFunction = KHR_DF_TRANSFER_UNSPECIFIED;
|
|
if (options.assignOETF.has_value()) {
|
|
if (options.assignOETF == KHR_DF_TRANSFER_SRGB) {
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionSRGB>();
|
|
} else {
|
|
assert(options.assignOETF == KHR_DF_TRANSFER_LINEAR);
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionLinear>();
|
|
}
|
|
colorSpaceInfo.usedInputTransferFunction = options.assignOETF.value();
|
|
target.format().setTransfer(options.assignOETF.value());
|
|
} else {
|
|
// Set image's OETF as indicated by metadata.
|
|
if (spec.format().transfer() != KHR_DF_TRANSFER_UNSPECIFIED) {
|
|
colorSpaceInfo.usedInputTransferFunction = spec.format().transfer();
|
|
switch (spec.format().transfer()) {
|
|
case KHR_DF_TRANSFER_LINEAR:
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionLinear>();
|
|
break;
|
|
case KHR_DF_TRANSFER_SRGB:
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionSRGB>();
|
|
break;
|
|
case KHR_DF_TRANSFER_ITU:
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionITU>();
|
|
break;
|
|
case KHR_DF_TRANSFER_PQ_EOTF:
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionBT2100_PQ_EOTF>();
|
|
break;
|
|
default:
|
|
fatal(rc::INVALID_FILE, "Transfer function {} used by input file \"{}\" is not supported by KTX. "
|
|
"Use --assign-oetf to specify a different one.",
|
|
toString(spec.format().transfer()), in.filename());
|
|
}
|
|
} else if (spec.format().iccProfileName().size()) {
|
|
fatal(rc::INVALID_FILE,
|
|
"Input file \"{}\" contains unsupported ICC profile \"{}\". Use --assign-oetf to specify a different one.",
|
|
in.filename(), spec.format().iccProfileName());
|
|
} else if (spec.format().oeGamma() > 0.0f) {
|
|
if (spec.format().oeGamma() > .45450f && spec.format().oeGamma() < .45460f) {
|
|
// N.B The previous loader matched oeGamma .45455 to the sRGB
|
|
// OETF and did not do an OETF transformation. In this loader
|
|
// we decode and reencode. Previous behavior can be obtained
|
|
// with the --assign_oetf option to toktx.
|
|
//
|
|
// This change results in 1 bit differences in the LSB of
|
|
// some color values noticeable only when directly comparing
|
|
// images produced before and after this change of loader.
|
|
warning("Converting gamma 2.2f to sRGB. Use --assign-oetf srgb to force treating input as sRGB.");
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionGamma>(spec.format().oeGamma());
|
|
} else if (spec.format().oeGamma() == 1.0) {
|
|
colorSpaceInfo.usedInputTransferFunction = KHR_DF_TRANSFER_LINEAR;
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionLinear>();
|
|
} else if (spec.format().oeGamma() > 0.0f) {
|
|
// We allow any gamma, there is not really a reason why we could not allow such input
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionGamma>(spec.format().oeGamma());
|
|
} else if (spec.format().oeGamma() == 0.0f) {
|
|
if (!in.formatName().compare("png")) {
|
|
// If 8-bit, treat as sRGB, otherwise treat as linear.
|
|
if (spec.format().channelBitLength() == 8) {
|
|
colorSpaceInfo.usedInputTransferFunction = KHR_DF_TRANSFER_SRGB;
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionSRGB>();
|
|
} else {
|
|
colorSpaceInfo.usedInputTransferFunction = KHR_DF_TRANSFER_LINEAR;
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionLinear>();
|
|
}
|
|
warning("Ignoring reported gamma of 0.0f in {}-bit PNG input file \"{}\". Handling as {}.",
|
|
spec.format().channelBitLength(), in.filename(), toString(colorSpaceInfo.usedInputTransferFunction));
|
|
} else {
|
|
fatal(rc::INVALID_FILE,
|
|
"Input file \"{}\" has gamma 0.0f. Use --assign-oetf to specify transfer function.");
|
|
}
|
|
} else {
|
|
if (!options.convertOETF.has_value()) {
|
|
fatal(rc::INVALID_FILE, "Gamma {} not automatically supported by KTX. Specify handing with "
|
|
"--convert-oetf or --assign-oetf.");
|
|
}
|
|
}
|
|
} else if (!in.formatName().compare("png")) {
|
|
// If 8-bit, treat as sRGB, otherwise treat as linear.
|
|
if (spec.format().channelBitLength() == 8) {
|
|
colorSpaceInfo.usedInputTransferFunction = KHR_DF_TRANSFER_SRGB;
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionSRGB>();
|
|
} else {
|
|
colorSpaceInfo.usedInputTransferFunction = KHR_DF_TRANSFER_LINEAR;
|
|
colorSpaceInfo.srcTransferFunction = std::make_unique<TransferFunctionLinear>();
|
|
}
|
|
warning("No transfer function can be determined from {}-bit PNG input file \"{}\", defaulting to {}. Use --assign-oetf to override.",
|
|
spec.format().channelBitLength(), in.filename(), toString(colorSpaceInfo.usedInputTransferFunction));
|
|
}
|
|
}
|
|
|
|
if (options.convertOETF.has_value()) {
|
|
target.format().setTransfer(options.convertOETF.value());
|
|
}
|
|
|
|
// Need to do color conversion if either the transfer functions don't match or the primaries
|
|
if (target.format().transfer() != colorSpaceInfo.usedInputTransferFunction ||
|
|
target.format().primaries() != colorSpaceInfo.usedInputPrimaries) {
|
|
if (colorSpaceInfo.srcTransferFunction == nullptr)
|
|
fatal(rc::INVALID_FILE,
|
|
"No transfer function can be determined from input file \"{}\". Use --assign-oetf to specify one.", in.filename());
|
|
|
|
switch (target.format().transfer()) {
|
|
case KHR_DF_TRANSFER_LINEAR:
|
|
colorSpaceInfo.dstTransferFunction = std::make_unique<TransferFunctionLinear>();
|
|
break;
|
|
case KHR_DF_TRANSFER_SRGB:
|
|
colorSpaceInfo.dstTransferFunction = std::make_unique<TransferFunctionSRGB>();
|
|
break;
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CommandCreate::checkSpecsMatch(const ImageInput& currentFile, const ImageSpec& firstSpec) {
|
|
const FormatDescriptor& firstFormat = firstSpec.format();
|
|
const FormatDescriptor& currentFormat = currentFile.spec().format();
|
|
|
|
if (currentFormat.transfer() != firstFormat.transfer()) {
|
|
if (options.assignOETF.has_value()) {
|
|
warning("Input image \"{}\" has different transfer function ({}) than the first image ({})"
|
|
" but will be treated identically as specified by the --assign-oetf option.",
|
|
currentFile.filename(), toString(currentFormat.transfer()), toString(firstFormat.transfer()));
|
|
} else if (options.convertOETF.has_value()) {
|
|
warning("Input image \"{}\" has different transfer function ({}) than the first image ({})"
|
|
" and thus will go through different transfer function conversion to the target transfer"
|
|
" function specified by the --convert-oetf option.",
|
|
currentFile.filename(), toString(currentFormat.transfer()), toString(firstFormat.transfer()));
|
|
} else {
|
|
fatal(rc::INVALID_FILE, "Input image \"{}\" has different transfer function ({}) than the first image ({})."
|
|
" Use --assign-oetf or --convert-oetf to specify handling and stop this error.",
|
|
currentFile.filename(), toString(currentFormat.transfer()), toString(firstFormat.transfer()));
|
|
}
|
|
}
|
|
|
|
if (currentFormat.oeGamma() != firstFormat.oeGamma()) {
|
|
auto currentGamma = currentFormat.oeGamma() != -1 ? std::to_string(currentFormat.oeGamma()) : "no gamma";
|
|
auto firstGamma = firstFormat.oeGamma() != -1 ? std::to_string(firstFormat.oeGamma()) : "no gamma";
|
|
if (options.assignOETF.has_value()) {
|
|
warning("Input image \"{}\" has different gamma ({}) than the first image ({})"
|
|
" but will be treated identically as specified by the --assign-oetf option.",
|
|
currentFile.filename(), currentGamma, firstGamma);
|
|
} else if (options.convertOETF.has_value()) {
|
|
warning("Input image \"{}\" has different gamma ({}) than the first image ({})"
|
|
" and thus will go through different transfer function conversion to the target transfer"
|
|
" function specified by the --convert-oetf option.",
|
|
currentFile.filename(), currentGamma, firstGamma);
|
|
} else {
|
|
fatal(rc::INVALID_FILE, "Input image \"{}\" has different gamma ({}) than the first image ({})."
|
|
" Use --assign-oetf or --convert-oetf to specify handling and stop this error.",
|
|
currentFile.filename(), currentGamma, firstGamma);
|
|
}
|
|
}
|
|
|
|
if (currentFormat.primaries() != firstFormat.primaries()) {
|
|
if (options.assignPrimaries.has_value()) {
|
|
warning("Input image \"{}\" has different primaries ({}) than the first image ({})"
|
|
" but will be treated identically as specified by the --assign-primaries option.",
|
|
currentFile.filename(), toString(currentFormat.primaries()), toString(firstFormat.primaries()));
|
|
} else if (options.convertPrimaries.has_value()) {
|
|
warning("Input image \"{}\" has different primaries ({}) than the first image ({})"
|
|
" and thus will go through different primaries conversion to the target primaries"
|
|
" specified by the --convert-primaries option.",
|
|
currentFile.filename(), toString(currentFormat.primaries()), toString(firstFormat.primaries()));
|
|
} else {
|
|
fatal(rc::INVALID_FILE, "Input image \"{}\" has different primaries ({}) than the first image ({})."
|
|
" Use --assign-primaries or --convert-primaries to specify handling and stop this error.",
|
|
currentFile.filename(), toString(currentFormat.primaries()), toString(firstFormat.primaries()));
|
|
}
|
|
}
|
|
|
|
if (currentFormat.channelCount() != firstFormat.channelCount()) {
|
|
warning("Input image \"{}\" has a different component count than the first image.", currentFile.filename());
|
|
}
|
|
}
|
|
|
|
} // namespace ktx
|
|
|
|
KTX_COMMAND_ENTRY_POINT(ktxCreate, ktx::CommandCreate)
|