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

1256 lines
60 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 "format_descriptor.h"
#include "formats.h"
#include "fragment_uri.h"
#include "sbufstream.h"
#include "utility.h"
#include "validate.h"
#include "transcode_utils.h"
#include "image.hpp"
#include "ktx.h"
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <unordered_map>
#include <cxxopts.hpp>
#include <fmt/ostream.h>
#include <fmt/printf.h>
#include "lodepng/lodepng.h"
#include "astc-encoder/Source/ThirdParty/tinyexr.h"
#include "astc-encoder/Source/astcenc.h"
// -------------------------------------------------------------------------------------------------
namespace ktx {
// -------------------------------------------------------------------------------------------------
/** @page ktx_extract ktx extract
@~English
Extract selected images from a KTX2 file.
@section ktx_extract_synopsis SYNOPSIS
ktx extract [option...] @e input-file @e output-path
@section ktx_extract_description DESCRIPTION
@b ktx @b extract can extract one or multiple images from the KTX2 file specified as the
@e input-file argument and, based on the format, save them as Raw, EXR or PNG image files
to the @e output-path.
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.
If the input file is invalid the first encountered validation error is displayed
to the stderr and the command exits with the relevant non-zero status code.
The @e output-path is interpreted as output filepath for single and output directory for
multi-image extracts.
When extracting multiple images with either '--all' or any of the 'all' args the
following naming is used for each output file:
<pre>output-path/output_level{}_face{}_layer{}_depth{}.extension</pre>
- Where the @e _level{} part is only present if the source texture has more than 1 level
- Where the @e _face{} part is only present if the source texture is cubemap or cubemap array (Cubemap)
- Where the @e _layer{} part is only present if the source texture is an array texture (Array)
- Where the @e _depth{} part is only present if the source texture baseDepth is more than 1 (3D)
- Where the @e {} is replaced with the numeric index of the given component starting from 0
- Where the @e extension part is "raw", "png" or "exr" based on the export format<br />
Note: The inclusion of the optional parts are determined by the source texture regardless of
which images are requested.
For non-raw exports the output image format is chosen to be the smallest related lossless
format:
- _UNORM formats exported as PNG with RGB/RGBA 8/16 bit
- _SINT/_UINT formats exported as EXR with R/RG/RGB/RGBA Half/Float/UInt
- _SFLOAT/_UFLOAT formats exported as EXR with R/RG/RGB/RGBA Half/Float/UInt
- D16_UNORM exported as PNG with luminance (Gray) 16 bit
- Other Depth/Stencil formats exported as EXR with D/S/DS Half/Float
@section ktx\_extract\_options OPTIONS
The following options are available:
<dl>
<dt>\--transcode &lt;target&gt;</dt>
<dd>Transcode the texture to the target format before executing the extract.
Requires the input file to be transcodable (it must be either BasisLZ
supercompressed or has UASTC color model in the DFD). This option matches the
functionality of the @ref ktx_transcode "ktx transcode" command.
If the target option is not set the r8, rg8, rgb8 or rgba8 target will be selected
based on the number of channels in the input texture.
Block compressed transcode targets can only be saved in raw format.
Case-insensitive. Possible options are:
etc-rgb | etc-rgba | eac-r11 | eac-rg11 | bc1 | bc3 | bc4 | bc5 | bc7 | astc |
r8 | rg8 | rgb8 | rgba8.
etc-rgb is ETC1; etc-rgba, eac-r11 and eac-rg11 are ETC2.
</dd>
</dl>
<dl>
<dt>\--uri &lt;uri&gt;</dt>
<dd>KTX Fragment URI. https://registry.khronos.org/KTX/specs/2.0/ktx-frag.html
</dd>
<dt>\--level [0-9]+ | all</dt>
<dd>Level to extract. When 'all' is used every level is exported. Defaults to 0.
</dd>
<dt>\--layer [0-9]+ | all</dt>
<dd>Layer to extract. When 'all' is used every layer is exported. Defaults to 0.
</dd>
<dt>\--face [0-9]+ | all</dt>
<dd>Face to extract. When 'all' is used every face is exported. Defaults to 0.
</dd>
<dt>\--depth [0-9]+ | all</dt>
<dd>Depth slice to extract. When 'all' is used every depth is exported. Defaults to 0.
</dd>
<dt>\--all</dt>
<dd>Extract every image slice from the texture.
</dd>
<dt>\--raw</dt>
<dd>Extract the raw image data without any conversion.
</dd>
</dl>
@snippet{doc} ktx/command.h command options_generic
@section ktx_extract_exitstatus EXIT STATUS
@snippet{doc} ktx/command.h command exitstatus
@section ktx_extract_history HISTORY
@par Version 4.0
- Initial version
@section ktx_extract_author AUTHOR
- Mátyás Császár [Vader], RasterGrid www.rastergrid.com
- Daniel Rákos, RasterGrid www.rastergrid.com
*/
class CommandExtract : public Command {
struct OptionsExtract {
inline static const char* kOutput = "output";
inline static const char* kStdout = "stdout";
inline static const char* kTranscode = "transcode";
inline static const char* kUri = "uri";
inline static const char* kLevel = "level";
inline static const char* kLayer = "layer";
inline static const char* kFace = "face";
inline static const char* kDepth = "depth";
inline static const char* kAll = "all";
inline static const char* kRaw = "raw";
std::string outputPath;
FragmentURI fragmentURI;
SelectorRange depth;
bool levelFlagUsed = false;
bool layerFlagUsed = false;
bool faceFlagUsed = false;
bool depthFlagUsed = false;
bool uriFlagUsed = false;
bool globalAll = false;
bool raw = false;
void init(cxxopts::Options& opts);
void process(cxxopts::Options& opts, cxxopts::ParseResult& args, Reporter& report);
};
Combine<OptionsExtract, OptionsTranscodeTarget<false>, OptionsSingleIn, OptionsGeneric> options;
public:
virtual int main(int argc, char* argv[]) override;
virtual void initOptions(cxxopts::Options& opts) override;
virtual void processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) override;
private:
void executeExtract();
void saveRawFile(std::string filepath, bool appendExtension, const char* data, std::size_t size);
void saveImageFile(std::string filepath, bool appendExtension, const char* data, std::size_t size, VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height);
void savePNG(std::string filepath, bool appendExtension, VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height, const char* data, std::size_t size);
void saveEXR(std::string filepath, bool appendExtension, VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height, int pixelType, const char* data, std::size_t size);
void saveEXR(std::string filepath, bool appendExtension, VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height, const std::vector<int>& pixelTypes, const char* data, std::size_t size);
void decodeAndSaveASTC(std::string filepath, bool appendExtension, VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height, const char* data, std::size_t size);
void unpackAndSave422(std::string filepath, bool appendExtension, VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height, const char* data, std::size_t size);
};
// -------------------------------------------------------------------------------------------------
int CommandExtract::main(int argc, char* argv[]) {
try {
parseCommandLine("ktx extract",
"Extract one or multiple images from the KTX2 file specified as the input-file argument\n"
" and, based on the format, save them as Raw, EXR or PNG image files to the output-path.",
argc, argv);
executeExtract();
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 CommandExtract::OptionsExtract::init(cxxopts::Options& opts) {
opts.add_options()
(kOutput, "Output filepath for single, output directory for multiple image export.", cxxopts::value<std::string>(), "<filepath>")
(kStdout, "Use stdout as the output file. (Using a single dash '-' as the output file has the same effect)")
(kTranscode, "Transcode the texture to the target format before executing the extract steps."
" Requires the input file to be transcodable."
" Block compressed transcode targets can only be saved in raw format."
" Case-insensitive."
"\nPossible options are:"
" etc-rgb | etc-rgba | eac-r11 | eac-rg11 | bc1 | bc3 | bc4 | bc5 | bc7 | astc |"
" r8 | rg8 | rgb8 | rgba8."
"\netc-rgb is ETC1; etc-rgba, eac-r11 and eac-rg11 are ETC2.",
cxxopts::value<std::string>(), "<target>")
(kUri, "KTX Fragment URI.", cxxopts::value<std::string>(), "<uri>")
(kLevel, "Level to extract. When 'all' is used every level is exported. Defaults to 0.", cxxopts::value<std::string>(), "[0-9]+ | all")
(kLayer, "Layer to extract. When 'all' is used every layer is exported. Defaults to 0.", cxxopts::value<std::string>(), "[0-9]+ | all")
(kFace, "Face to extract. When 'all' is used every face is exported. Defaults to 0.", cxxopts::value<std::string>(), "[0-5] | all")
(kDepth, "Depth slice to extract. When 'all' is used every depth is exported. Defaults to 0.", cxxopts::value<std::string>(), "[0-9]+ | all")
(kAll, "Extract every image slice from the texture.")
(kRaw, "Extract the raw image data without any conversion.");
}
void CommandExtract::OptionsExtract::process(cxxopts::Options&, cxxopts::ParseResult& args, Reporter& report) {
if (args.count(kOutput))
outputPath = args[kOutput].as<std::string>();
else
report.fatal_usage("Missing output file or directory path.");
const auto parseSelector = [&](const std::string& name, bool& found) -> std::optional<SelectorRange> {
if (!args[name].count())
return std::nullopt;
const auto str = to_lower_copy(args[name].as<std::string>());
try {
found = true;
return str == kAll ? SelectorRange(all) : SelectorRange(std::stoi(str));
} catch (const std::invalid_argument&) {
report.fatal_usage("Invalid {} value \"{}\". The value must be a either a number or \"all\".", name, str);
} catch (const std::out_of_range& e) {
report.fatal_usage("Out of range {} value \"{}\": {}.", name, str, e.what());
}
return std::nullopt;
};
auto level = parseSelector(kLevel, levelFlagUsed);
auto layer = parseSelector(kLayer, layerFlagUsed);
auto face = parseSelector(kFace, faceFlagUsed);
auto depth_ = parseSelector(kDepth, depthFlagUsed);
raw = args[kRaw].as<bool>();
globalAll = args[kAll].as<bool>();
if (globalAll) {
if (level)
report.fatal_usage("Conflicting options: --level cannot be used with --all.");
if (layer)
report.fatal_usage("Conflicting options: --layer cannot be used with --all.");
if (face)
report.fatal_usage("Conflicting options: --face cannot be used with --all.");
if (depth_)
report.fatal_usage("Conflicting options: --depth cannot be used with --all.");
level = all;
layer = all;
face = all;
depth_ = all;
}
if (globalAll && outputPath == "-")
report.fatal_usage("stdout cannot be used with multi-output '--all' extract.");
if (level == all && outputPath == "-")
report.fatal_usage("stdout cannot be used with multi-output '--level all' extract.");
if (layer == all && outputPath == "-")
report.fatal_usage("stdout cannot be used with multi-output '--layer all' extract.");
if (face == all && outputPath == "-")
report.fatal_usage("stdout cannot be used with multi-output '--face all' extract.");
if (depth_ == all && outputPath == "-")
report.fatal_usage("stdout cannot be used with multi-output '--depth all' extract.");
if (args[kUri].count()) {
uriFlagUsed = true;
if (globalAll)
report.fatal_usage("Conflicting options: --all cannot be used with --uri.");
if (levelFlagUsed)
report.fatal_usage("Conflicting options: --level cannot be used with --uri.");
if (layerFlagUsed)
report.fatal_usage("Conflicting options: --layer cannot be used with --uri.");
if (faceFlagUsed)
report.fatal_usage("Conflicting options: --face cannot be used with --uri.");
try {
fragmentURI = parseFragmentURI(args[kUri].as<std::string>());
} catch (const std::exception& e) {
report.fatal_usage("Failed to parse Fragment URI: {}", e.what());
}
const auto isMultiOutputFragmentURI =
(!fragmentURI.mip.is_undefined() && fragmentURI.mip.is_multi()) ||
(!fragmentURI.stratal.is_undefined() && fragmentURI.stratal.is_multi()) ||
(!fragmentURI.facial.is_undefined() && fragmentURI.facial.is_multi());
if (isMultiOutputFragmentURI && outputPath == "-")
report.fatal_usage("stdout cannot be used with multi-output '--uri' extract.");
} else {
// Merge every other selection method into the fragmentURI
fragmentURI.mip = level.value_or(SelectorRange(0));
fragmentURI.stratal = layer.value_or(SelectorRange(0));
fragmentURI.facial = face.value_or(SelectorRange(0));
}
this->depth = depth_.value_or(SelectorRange(0));
}
void CommandExtract::initOptions(cxxopts::Options& opts) {
options.init(opts);
opts.parse_positional({"input-file", OptionsExtract::kOutput});
opts.positional_help("<input-file> <output>");
}
void CommandExtract::processOptions(cxxopts::Options& opts, cxxopts::ParseResult& args) {
options.process(opts, args, *this);
if (!options.raw && options.transcodeTarget) {
if (options.transcodeTarget != KTX_TTF_RGBA32)
fatal_usage("Transcode to \"{}\" for non-raw extract is not supported. "
"For PNG/EXR output only r8, rg8, rgb8 and rgba8 are supported.", options.transcodeTargetName);
}
}
void CommandExtract::executeExtract() {
InputStream inputStream(options.inputFilepath, *this);
validateToolInput(inputStream, fmtInFile(options.inputFilepath), *this);
KTXTexture2 texture{nullptr};
StreambufStream<std::streambuf*> ktx2Stream{inputStream->rdbuf(), std::ios::in | std::ios::binary};
auto ret = ktxTexture2_CreateFromStream(ktx2Stream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, texture.pHandle());
if (ret != KTX_SUCCESS)
fatal(rc::INVALID_FILE, "Failed to create KTX2 texture: {}", ktxErrorString(ret));
// CLI request validation
if (!options.fragmentURI.mip.validate(texture->numLevels))
fatal(rc::INVALID_FILE, "Requested level index {} is missing. The input file only has {} level(s).",
options.fragmentURI.mip, texture->numLevels);
if (((options.uriFlagUsed && !options.fragmentURI.stratal.is_undefined()) || options.layerFlagUsed) && !texture->isArray) {
if (options.fragmentURI.stratal == all)
fatal(rc::INVALID_FILE, "Requested all layers from a non-array texture.");
else
fatal(rc::INVALID_FILE, "Requested layer index {} from a non-array texture.", options.fragmentURI.stratal);
}
if (!options.fragmentURI.stratal.validate(texture->numLayers))
fatal(rc::INVALID_FILE, "Requested layer index {} is missing. The input file only has {} layer(s).",
options.fragmentURI.stratal, texture->numLayers);
if (((options.uriFlagUsed && !options.fragmentURI.facial.is_undefined()) || options.faceFlagUsed) && !texture->isCubemap) {
if (options.fragmentURI.facial == all)
fatal(rc::INVALID_FILE, "Requested all faces from a non-cubemap texture.");
else
fatal(rc::INVALID_FILE, "Requested face index {} from a non-cubemap texture.", options.fragmentURI.facial);
}
if (!options.fragmentURI.facial.validate(texture->numFaces))
fatal(rc::INVALID_FILE, "Requested face index {} is missing. The input file only has {} face(s).",
options.fragmentURI.facial, texture->numFaces);
if (!options.globalAll && options.depthFlagUsed && texture->numDimensions != 3) {
if (options.depth == all)
fatal(rc::INVALID_FILE, "Requested all depth slices from a non-3D texture.");
else
fatal(rc::INVALID_FILE, "Requested depth slice index {} from a non-3D texture.", options.depth);
}
// Transcoding
if (ktxTexture2_NeedsTranscoding(texture)) {
texture = transcode(std::move(texture), options, *this);
} else if (options.transcodeTarget) {
fatal(rc::INVALID_FILE, "Requested transcode \"{}\" but the KTX file is not transcodable.",
options.transcodeTargetName);
}
const auto format = createFormatDescriptor(texture->pDfd);
const auto blockSizeZ = format.basic.texelBlockDimension2 + 1u;
const auto lastExportedLevel = options.fragmentURI.mip == all ? texture->numLevels - 1 : options.fragmentURI.mip.last();
const auto lastExportedLevelDepthCount = std::max(1u, ceil_div(texture->baseDepth, blockSizeZ) >> lastExportedLevel);
if (options.depthFlagUsed && options.depth != all && options.depth.last() > lastExportedLevelDepthCount)
fatal(rc::INVALID_FILE, "Requested depth slice index {} is missing. The input file only has {} depth slice(s) in level {}.",
options.depth, lastExportedLevelDepthCount, lastExportedLevel);
// Setup output directory
const auto isMultiOutput =
(!options.fragmentURI.mip.is_undefined() && options.fragmentURI.mip.is_multi()) ||
(!options.fragmentURI.stratal.is_undefined() && options.fragmentURI.stratal.is_multi()) ||
(!options.fragmentURI.facial.is_undefined() && options.fragmentURI.facial.is_multi()) ||
((options.globalAll || options.depthFlagUsed) && options.depth.is_multi());
try {
const auto outputPath = std::filesystem::path(DecodeUTF8Path(options.outputPath));
if (isMultiOutput) {
if (std::filesystem::exists(outputPath) && !std::filesystem::is_directory(outputPath))
fatal_usage("Specified output path must be a directory for multi-output extract: \"{}\".", options.outputPath);
std::filesystem::create_directories(outputPath);
} else {
if (outputPath.has_parent_path())
std::filesystem::create_directories(outputPath.parent_path());
}
} catch (const std::filesystem::filesystem_error& e) {
fatal(rc::IO_FAILURE, "Failed to create the output directory \"{}\": {}.", e.path1().generic_string(), e.what());
}
// Iterate
for (uint32_t levelIndex = 0; levelIndex < texture->numLevels; ++levelIndex) {
if (options.fragmentURI.mip.is_undefined() ?
levelIndex != 0 :
!options.fragmentURI.mip.contains(levelIndex))
continue;
std::size_t imageSize = ktxTexture_GetImageSize(texture, levelIndex);
const auto imageWidth = std::max(1u, texture->baseWidth >> levelIndex);
const auto imageHeight = std::max(1u, texture->baseHeight >> levelIndex);
const auto imageDepth = std::max(1u, texture->baseDepth >> levelIndex);
for (uint32_t layerIndex = 0; layerIndex < texture->numLayers; ++layerIndex) {
if (options.fragmentURI.stratal.is_undefined() ?
layerIndex != 0 :
!options.fragmentURI.stratal.contains(layerIndex))
continue;
for (uint32_t faceIndex = 0; faceIndex < texture->numFaces; ++faceIndex) {
if (options.fragmentURI.facial.is_undefined() ?
faceIndex != 0 :
!options.fragmentURI.facial.contains(faceIndex))
continue;
if (imageDepth > 1 && !options.globalAll && !options.depthFlagUsed && options.raw) {
// If the texture type is 3D / 3D Array and the "all" or "depth" option is not set,
// the whole 3D block of pixel data is selected according to the "level" and "layer"
// option. This extraction path requires the "raw" option to be enabled.
const auto outputFilepath = !isMultiOutput ? options.outputPath :
fmt::format("{}/output{}{}{}.raw",
options.outputPath,
texture->numLevels > 1 ? fmt::format("_level{}", levelIndex) : "",
texture->isCubemap ? fmt::format("_face{}", faceIndex) : "",
texture->isArray ? fmt::format("_layer{}", layerIndex) : ""
// Depth is not part of the name as the whole 3D image is raw exported
);
OutputStream file(outputFilepath, *this);
for (uint32_t depthIndex = 0; depthIndex < imageDepth; ++depthIndex) {
ktx_size_t imageOffset;
ktxTexture_GetImageOffset(texture, levelIndex, layerIndex, faceIndex + depthIndex, &imageOffset);
const char* imageData = reinterpret_cast<const char*>(texture->pData) + imageOffset;
file.write(imageData, imageSize, *this);
}
continue;
}
// Iterate z_slice_of_blocks
for (uint32_t depthIndex = 0; depthIndex < ceil_div(imageDepth, blockSizeZ); ++depthIndex) {
if (!options.depth.contains(depthIndex))
continue; // Skip
ktx_size_t imageOffset;
ktxTexture_GetImageOffset(texture, levelIndex, layerIndex, faceIndex + depthIndex, &imageOffset);
char* depthSliceData = reinterpret_cast<char*>(texture->pData) + imageOffset;
const auto outputFilepath = !isMultiOutput ? options.outputPath :
fmt::format("{}/output{}{}{}{}",
options.outputPath,
texture->numLevels > 1 ? fmt::format("_level{}", levelIndex) : "",
texture->isCubemap ? fmt::format("_face{}", faceIndex) : "",
texture->isArray ? fmt::format("_layer{}", layerIndex) : "",
texture->baseDepth > 1 ? fmt::format("_depth{}", depthIndex) : ""
);
if (options.raw) {
saveRawFile(outputFilepath, isMultiOutput, depthSliceData, imageSize);
} else {
saveImageFile(outputFilepath, isMultiOutput, depthSliceData, imageSize,
static_cast<VkFormat>(texture->vkFormat), format, imageWidth, imageHeight);
}
}
}
}
}
if (ret != KTX_SUCCESS)
fatal(rc::INVALID_FILE, "Failed to iterate KTX2 texture: {}", ktxErrorString(ret));
}
void CommandExtract::decodeAndSaveASTC(std::string filepath, bool appendExtension, VkFormat vkFormat, const FormatDescriptor& format,
uint32_t width, uint32_t height, const char* compressedData, std::size_t compressedSize) {
const auto threadCount = 1u;
const auto blockSizeX = format.basic.texelBlockDimension0 + 1u;
const auto blockSizeY = format.basic.texelBlockDimension1 + 1u;
const auto blockSizeZ = format.basic.texelBlockDimension2 + 1u;
static constexpr astcenc_swizzle swizzle{ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A};
astcenc_error ec = ASTCENC_SUCCESS;
const astcenc_profile profile = isFormatSRGB(vkFormat) ? ASTCENC_PRF_LDR_SRGB : ASTCENC_PRF_LDR;
astcenc_config config{};
ec = astcenc_config_init(profile, blockSizeX, blockSizeY, blockSizeZ, ASTCENC_PRE_MEDIUM, ASTCENC_FLG_DECOMPRESS_ONLY, &config);
if (ec != ASTCENC_SUCCESS)
fatal(rc::RUNTIME_ERROR, "ASTC Codec config init failed: {}", astcenc_get_error_string(ec));
struct ASTCencStruct {
astcenc_context* context = nullptr;
~ASTCencStruct() {
astcenc_context_free(context);
}
} astcenc;
astcenc_context*& context = astcenc.context;
ec = astcenc_context_alloc(&config, threadCount, &context);
if (ec != ASTCENC_SUCCESS)
fatal(rc::RUNTIME_ERROR, "ASTC Codec context alloc failed: {}", astcenc_get_error_string(ec));
astcenc_image image{};
image.dim_x = width;
image.dim_y = height;
image.dim_z = 1; // 3D ASTC formats are currently not supported
const auto uncompressedSize = width * height * 4 * sizeof(uint8_t);
const auto uncompressedBuffer = std::make_unique<uint8_t[]>(uncompressedSize);
auto* bufferPtr = uncompressedBuffer.get();
image.data = reinterpret_cast<void**>(&bufferPtr);
image.data_type = ASTCENC_TYPE_U8;
ec = astcenc_decompress_image(context, reinterpret_cast<const uint8_t*>(compressedData), compressedSize, &image, &swizzle, 0);
if (ec != ASTCENC_SUCCESS)
fatal(rc::RUNTIME_ERROR, "ASTC Codec decompress failed: {}", astcenc_get_error_string(ec));
astcenc_decompress_reset(context);
const auto uncompressedVkFormat = isFormatSRGB(vkFormat) ?
VK_FORMAT_R8G8B8A8_SRGB :
VK_FORMAT_R8G8B8A8_UNORM;
saveImageFile(
std::move(filepath),
appendExtension,
reinterpret_cast<const char*>(uncompressedBuffer.get()),
uncompressedSize,
uncompressedVkFormat,
createFormatDescriptor(uncompressedVkFormat, *this),
width,
height);
}
using namespace imageio;
void CommandExtract::unpackAndSave422(std::string filepath, bool appendExtension,
VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height,
const char* data, std::size_t size) {
(void) vkFormat;
assert(format.model() == KHR_DF_MODEL_YUVSDA);
assert(format.find(KHR_DF_CHANNEL_YUVSDA_Y));
// Create a custom format with the same precision but with only 3 channels
// Reuse similar 4 channel VkFormats and drop the last channel (There is no RGB variant of 10X6 and 12X4)
const auto precision = format.find(KHR_DF_CHANNEL_YUVSDA_Y)->bitLength + 1u;
auto unpackedFormat = createFormatDescriptor(
precision == 8 ? VK_FORMAT_R8G8B8A8_UNORM :
precision == 10 ? VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16 :
precision == 12 ? VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16 :
precision == 16 ? VK_FORMAT_R16G16B16A16_UNORM :
VK_FORMAT_UNDEFINED, *this);
unpackedFormat.removeLastChannel();
// 1 pixel (block) with 4 channel is unpacked to 2 pixel with 3 channels: Y0,Y1,U,V -> R0,G0,B0,R1,G1,B1
const auto blockYUVBytes = format.pixelByteCount();
const auto blockDimensionX = format.basic.texelBlockDimension0 + 1;
const auto blockDimensionY = format.basic.texelBlockDimension1 + 1;
const auto pixelBytes = unpackedFormat.pixelByteCount();
const auto channelBytes = pixelBytes / 3;
assert(format.sampleCount() == 4);
assert(format.basic.texelBlockDimension0 + 1 == 2);
assert(format.basic.texelBlockDimension1 + 1 == 1);
assert(format.basic.texelBlockDimension2 + 1 == 1);
assert(format.basic.texelBlockDimension3 + 1 == 1);
assert(size == width * height * blockYUVBytes / blockDimensionX / blockDimensionY);
(void) size;
(void) blockDimensionY;
uint32_t y0Offset = 0;
uint32_t y0Bits = 0;
uint32_t y0PositionX = 0;
uint32_t y1Offset = 0;
uint32_t y1Bits = 0;
uint32_t y1PositionX = 0;
uint32_t uOffset = 0;
uint32_t uBits = 0;
uint32_t uPositionX = 0;
uint32_t vOffset = 0;
uint32_t vBits = 0;
uint32_t vPositionX = 0;
for (const auto& sample : format.samples) {
switch (sample.channelType) {
case KHR_DF_CHANNEL_YUVSDA_Y:
if (y0Bits != 0) {
y1Offset = sample.bitOffset;
y1Bits = sample.bitLength + 1;
y1PositionX = sample.samplePosition0;
} else {
y0Offset = sample.bitOffset;
y0Bits = sample.bitLength + 1;
y0PositionX = sample.samplePosition0;
}
break;
case KHR_DF_CHANNEL_YUVSDA_U:
uOffset = sample.bitOffset;
uBits = sample.bitLength + 1;
uPositionX = sample.samplePosition0;
break;
case KHR_DF_CHANNEL_YUVSDA_V:
vOffset = sample.bitOffset;
vBits = sample.bitLength + 1;
vPositionX = sample.samplePosition0;
break;
default:
assert(false && "Unsupported channel type");
}
}
if (y0PositionX > y1PositionX) {
// Ensure that y0 (as we refer to) is the left sample
std::swap(y0Offset, y1Offset);
std::swap(y0Bits, y1Bits);
std::swap(y0PositionX, y1PositionX);
}
assert(precision == y0Bits);
assert(precision == y1Bits);
assert(precision == uBits);
assert(precision == vBits);
const float positionY0 = static_cast<float>(y0PositionX * blockDimensionX) / 256.f;
const float positionY1 = static_cast<float>(y1PositionX * blockDimensionX) / 256.f;
const float positionU = static_cast<float>(uPositionX * blockDimensionX) / 256.f;
const float positionV = static_cast<float>(vPositionX * blockDimensionX) / 256.f;
const float blockSize = static_cast<float>(blockDimensionX);
std::vector<char> unpackedData(width * height * pixelBytes);
const auto blockCountX = width / blockDimensionX;
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < blockCountX; x++) {
const char* rawYUVBlockLeft = data + (y * blockCountX + (x == 0 ? 0 : x - 1)) * blockYUVBytes;
const char* rawYUVBlock = data + (y * blockCountX + x) * blockYUVBytes;
const char* rawYUVBlockRight = data + (y * blockCountX + (x == blockCountX - 1 ? blockCountX - 1 : x + 1)) * blockYUVBytes;
char* pixel0 = unpackedData.data() + (y * width + x * 2) * pixelBytes;
char* pixel1 = unpackedData.data() + (y * width + x * 2 + 1) * pixelBytes;
const float valueLeftY1 = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlockLeft, y1Offset, y1Bits), y1Bits);
const float valueLeftU = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlockLeft, uOffset, uBits), uBits);
const float valueLeftV = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlockLeft, vOffset, vBits), vBits);
const float valueY0 = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlock, y0Offset, y0Bits), y0Bits);
const float valueY1 = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlock, y1Offset, y1Bits), y1Bits);
const float valueU = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlock, uOffset, uBits), uBits);
const float valueV = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlock, vOffset, vBits), vBits);
const float valueRightY0 = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlockRight, y0Offset, y0Bits), y0Bits);
const float valueRightU = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlockRight, uOffset, uBits), uBits);
const float valueRightV = convertUNORMToFloat(extract_bits<uint32_t>(rawYUVBlockRight, vOffset, vBits), vBits);
const auto interpolateY = [](float pos, float pos0, float value0, float pos1, float value1, float pos2, float value2, float pos3, float value3) {
if (pos < pos1)
return remap(pos, pos0, pos1, value0, value1);
else if (pos < pos2)
return remap(pos, pos1, pos2, value1, value2);
else
return remap(pos, pos2, pos3, value2, value3);
};
const auto interpolateUV = [](float pos, float pos0, float value0, float pos1, float value1, float pos2, float value2) {
if (pos < pos1)
return remap(pos, pos0, pos1, value0, value1);
else
return remap(pos, pos1, pos2, value1, value2);
};
const auto setPixel = [&](auto* pixel, float pos) {
const auto r = convertFloatToUNORM(interpolateUV(pos,
positionV - blockSize, valueLeftV,
positionV, valueV,
positionV + blockSize, valueRightV), uBits);
const auto g = convertFloatToUNORM(interpolateY(
pos,
positionY1 - blockSize, valueLeftY1,
positionY0, valueY0,
positionY1, valueY1,
positionY0 + blockSize, valueRightY0), y0Bits);
const auto b = convertFloatToUNORM(interpolateUV(pos,
positionU - blockSize, valueLeftU,
positionU, valueU,
positionU + blockSize, valueRightU), uBits);
const auto offsetToUsedBytes = is_big_endian ? sizeof(r) - channelBytes : 0u;
std::memcpy(pixel + 0 * channelBytes, &r + offsetToUsedBytes, channelBytes);
std::memcpy(pixel + 1 * channelBytes, &g + offsetToUsedBytes, channelBytes);
std::memcpy(pixel + 2 * channelBytes, &b + offsetToUsedBytes, channelBytes);
};
setPixel(pixel0, 0.5f);
setPixel(pixel1, 1.5f);
}
}
// Save unpacked data as RGBSDA with the custom format
savePNG(filepath, appendExtension, VK_FORMAT_UNDEFINED, unpackedFormat, width, height,
reinterpret_cast<const char*>(unpackedData.data()),
unpackedData.size() * sizeof(decltype(unpackedData)::value_type));
}
void CommandExtract::saveRawFile(std::string filepath, bool appendExtension, const char* data, std::size_t size) {
if (appendExtension && filepath != "-")
filepath += ".raw";
OutputStream file(filepath, *this);
file.write(data, size, *this);
}
void CommandExtract::savePNG(std::string filepath, bool appendExtension,
VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height,
const char* data, std::size_t size) {
if (appendExtension && filepath != "-")
filepath += ".png";
const auto depthFormat = (vkFormat == VK_FORMAT_D16_UNORM);
const auto alphaFormat = (vkFormat == VK_FORMAT_A8_UNORM_KHR);
uint32_t packedChannelCount = 0;
uint32_t unpackedChannelCount = 3;
LodePNGColorType colorType = LCT_RGB;
uint32_t bitDepth = 8;
uint32_t srcOffsets[4] = {};
uint32_t srcBits[4] = {};
uint32_t dstChannels[4] = {};
auto addChannel = [&](uint32_t dstChannel, const FormatDescriptor::sample* sample) {
srcOffsets[packedChannelCount] = sample->bitOffset;
srcBits[packedChannelCount] = sample->bitLength + 1;
dstChannels[packedChannelCount] = dstChannel;
packedChannelCount++;
if (sample->bitLength >= 8) {
bitDepth = 16;
}
};
lodepng::State state{};
if (format.model() != KHR_DF_MODEL_RGBSDA)
fatal(rc::NOT_SUPPORTED, "PNG saving is not supported for {} with {}.", toString(format.model()), toString(vkFormat));
if (const auto sample = format.find(KHR_DF_CHANNEL_RGBSDA_R)) {
addChannel(0, sample);
}
if (const auto sample = format.find(KHR_DF_CHANNEL_RGBSDA_G)) {
addChannel(1, sample);
}
if (const auto sample = format.find(KHR_DF_CHANNEL_RGBSDA_B)) {
addChannel(2, sample);
}
if (const auto sample = format.find(KHR_DF_CHANNEL_RGBSDA_A)) {
if (alphaFormat) {
// For alpha-only formats we use grey-alpha
unpackedChannelCount = 2;
colorType = LCT_GREY_ALPHA;
addChannel(1, sample);
} else {
unpackedChannelCount = 4;
colorType = LCT_RGBA;
addChannel(3, sample);
}
}
if (const auto sample = format.find(KHR_DF_CHANNEL_RGBSDA_D)) {
if (!depthFormat)
fatal(rc::NOT_SUPPORTED, "PNG saving encountered unexpected depth channel in non-depth format {}.", toString(vkFormat));
// For depth-only formats we use grey
unpackedChannelCount = 1;
colorType = LCT_GREY;
addChannel(0, sample);
}
const auto byteDepth = bitDepth / 8u;
const auto pixelBytes = format.pixelByteCount();
assert(bitDepth == 8 || bitDepth == 16);
assert(size == width * height * pixelBytes); (void) size;
state.info_raw.bitdepth = bitDepth;
state.info_png.color.colortype = colorType;
state.info_png.color.bitdepth = bitDepth;
state.info_raw.colortype = colorType;
// Include sBit chunk if needed
const auto includeSBits =
(srcBits[0] != 0 && srcBits[0] != bitDepth) ||
(srcBits[1] != 0 && srcBits[1] != bitDepth) ||
(srcBits[2] != 0 && srcBits[2] != bitDepth) ||
(srcBits[3] != 0 && srcBits[3] != bitDepth);
if (includeSBits) {
state.info_png.sbit_defined = true;
state.info_png.sbit_r = srcBits[0] == 0 ? bitDepth : srcBits[0];
state.info_png.sbit_g = srcBits[1] == 0 ? bitDepth : srcBits[1];
state.info_png.sbit_b = srcBits[2] == 0 ? bitDepth : srcBits[2];
state.info_png.sbit_a = srcBits[3] == 0 ? bitDepth : srcBits[3];
}
std::vector<unsigned char> unpackedImage(width * height * unpackedChannelCount * byteDepth);
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
const char* rawPixel = data + (y * width + x) * pixelBytes;
for (uint32_t packedChannelIndex = 0; packedChannelIndex < packedChannelCount; ++packedChannelIndex) {
uint32_t offset = srcOffsets[packedChannelIndex];
uint32_t bits = srcBits[packedChannelIndex];
uint32_t dstChannel = dstChannels[packedChannelIndex];
const uint32_t value = convertUNORM(extract_bits<uint32_t>(rawPixel, offset, bits), bits, bitDepth);
if (byteDepth == 1) {
uint8_t temp = static_cast<uint8_t>(value);
std::memcpy(unpackedImage.data() + (y * width * unpackedChannelCount + x * unpackedChannelCount + dstChannel) * byteDepth, &temp, byteDepth);
} else if (byteDepth == 2) {
uint16_t temp = static_cast<uint16_t>(value);
if (!is_big_endian)
temp = byteswap(temp); // LodePNG Uses big endian input
std::memcpy(unpackedImage.data() + (y * width * unpackedChannelCount + x * unpackedChannelCount + dstChannel) * byteDepth, &temp, byteDepth);
}
}
}
}
// Include sRGB chunk if needed
if (format.transfer() == KHR_DF_TRANSFER_SRGB) {
state.info_png.srgb_defined = 1;
state.info_png.srgb_intent = 0;
}
// Output primaries as cHRM chunk
Primaries primaries;
bool foundPrimaries = getPrimaries(format.primaries(), &primaries);
if (foundPrimaries) {
state.info_png.chrm_defined = 1;
state.info_png.chrm_red_x = (unsigned int)(100000 * primaries.Rx);
state.info_png.chrm_red_y = (unsigned int)(100000 * primaries.Ry);
state.info_png.chrm_green_x = (unsigned int)(100000 * primaries.Gx);
state.info_png.chrm_green_y = (unsigned int)(100000 * primaries.Gy);
state.info_png.chrm_blue_x = (unsigned int)(100000 * primaries.Bx);
state.info_png.chrm_blue_y = (unsigned int)(100000 * primaries.By);
state.info_png.chrm_white_x = (unsigned int)(100000 * primaries.Wx);
state.info_png.chrm_white_y = (unsigned int)(100000 * primaries.Wy);
}
std::vector<unsigned char> png;
auto error = lodepng::encode(png, unpackedImage, width, height, state);
if (error)
fatal(rc::INVALID_FILE, "PNG Encoder error {}: {}.", error, lodepng_error_text(error));
OutputStream file(filepath, *this);
file.write(reinterpret_cast<const char*>(png.data()), png.size(), *this);
}
void CommandExtract::saveEXR(std::string filepath, bool appendExtension,
VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height,
int pixelType, const char* data, std::size_t size) {
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height,
std::vector<int>(vkFormat == VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 ? 3 : format.channelCount(), pixelType),
data, size);
}
void CommandExtract::saveEXR(std::string filepath, bool appendExtension,
VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height,
const std::vector<int>& pixelTypes, const char* data, std::size_t size) {
assert(!format.samples.empty());
if (appendExtension && filepath != "-")
filepath += ".exr";
struct Channel {
uint32_t offset;
uint32_t bits;
const char* name;
bool isFloat;
bool isSigned;
bool isNormalized;
};
std::vector<Channel> channels;
if (format.model() == KHR_DF_MODEL_RGBSDA) {
const auto addChannel = [&](auto channelType, const char* channelName) {
if (const auto sample = format.find(channelType))
channels.push_back({
sample->bitOffset,
sample->bitLength + 1u,
channelName,
sample->qualifierFloat != 0,
sample->qualifierSigned != 0,
sample->upper != (sample->qualifierFloat != 0 ? bit_cast<uint32_t>(1.0f) : 1u)
});
};
// Must be ABGR order, since most of EXR viewers expect this channel order by convention.
addChannel(KHR_DF_CHANNEL_RGBSDA_A, "A");
addChannel(KHR_DF_CHANNEL_RGBSDA_D, "D");
addChannel(KHR_DF_CHANNEL_RGBSDA_S, "S");
addChannel(KHR_DF_CHANNEL_RGBSDA_B, "B");
addChannel(KHR_DF_CHANNEL_RGBSDA_G, "G");
addChannel(KHR_DF_CHANNEL_RGBSDA_R, "R");
// } else if (formatDescriptor.model() == KHR_DF_MODEL_YUVSDA) {
// Other color model support would come here
} else {
fatal(rc::NOT_SUPPORTED, "EXR saving is unsupported for {} with {}.", toString(format.model()), toString(vkFormat));
}
const auto pixelBytes = format.pixelByteCount();
const auto numChannels = vkFormat == VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 ? 3 : format.channelCount();
assert(size == width * height * pixelBytes); (void) size;
assert(numChannels == pixelTypes.size());
// Image data is prepared with either floats or uint32
// (half output is will be converted from float by TinyEXR during save)
std::vector<std::vector<uint32_t>> images(numChannels);
std::array<uint32_t*, 4> imagePtrs{};
for (uint32_t i = 0; i < numChannels; ++i)
images[i].resize(width * height);
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
const char* rawPixel = data + (y * width + x) * pixelBytes;
if (vkFormat == VK_FORMAT_E5B9G9R9_UFLOAT_PACK32) {
// Special case for VK_FORMAT_E5B9G9R9_UFLOAT_PACK32
assert(numChannels == 3);
assert(pixelTypes[0] == TINYEXR_PIXELTYPE_HALF);
uint32_t pixel;
std::memcpy(&pixel, rawPixel, sizeof(uint32_t));
const auto values = glm::unpackF3x9_E1x5(pixel);
images[2][y * width + x] = bit_cast<uint32_t>(values.r);
images[1][y * width + x] = bit_cast<uint32_t>(values.g);
images[0][y * width + x] = bit_cast<uint32_t>(values.b);
} else {
for (uint32_t c = 0; c < numChannels; c++) {
const auto& channel = channels[c];
const auto offset = channel.offset;
const auto bits = channel.bits;
const auto value = extract_bits<uint32_t>(rawPixel, offset, bits);
auto& target = images[c][y * width + x];
if (pixelTypes[c] == TINYEXR_PIXELTYPE_FLOAT || pixelTypes[c] == TINYEXR_PIXELTYPE_HALF) {
if (channel.isFloat) {
if (channel.isSigned)
target = bit_cast<uint32_t>(convertSFloatToFloat(value, bits));
else
target = bit_cast<uint32_t>(convertUFloatToFloat(value, bits));
} else {
if (channel.isNormalized) {
if (channel.isSigned)
target = bit_cast<uint32_t>(convertSNORMToFloat(value, bits));
else
target = bit_cast<uint32_t>(convertUNORMToFloat(value, bits));
} else {
if (channel.isSigned)
target = bit_cast<uint32_t>(convertSIntToFloat(value, bits));
else
target = bit_cast<uint32_t>(convertUIntToFloat(value, bits));
}
}
} else if (pixelTypes[c] == TINYEXR_PIXELTYPE_UINT) {
if (channel.isFloat && channel.isSigned) {
target = convertSFloatToUInt(value, bits);
} else if (channel.isFloat && !channel.isSigned) {
target = convertUFloatToUInt(value, bits);
} else if (!channel.isFloat && channel.isSigned) {
target = convertSIntToUInt(value, bits);
} else if (!channel.isFloat && !channel.isSigned) {
target = convertUIntToUInt(value, bits);
}
} else
assert(false && "Internal error");
}
}
}
}
struct EXRStruct {
EXRHeader header;
EXRImage image;
std::vector<EXRAttribute> attributes;
const char* err = nullptr;
unsigned char* fileData = nullptr;
EXRStruct() {
InitEXRHeader(&header);
InitEXRImage(&image);
}
~EXRStruct() {
if (header.custom_attributes != nullptr) {
free(header.custom_attributes);
header.custom_attributes = nullptr;
header.num_custom_attributes = 0;
}
image.images = nullptr; // null out the images to stop TinyEXR from trying to free them
FreeEXRImage(&image);
FreeEXRHeader(&header);
FreeEXRErrorMessage(err);
free(fileData);
}
void addAttributesToHeader() {
header.num_custom_attributes = static_cast<int>(attributes.size());
header.custom_attributes = (EXRAttribute*)malloc(sizeof(EXRAttribute) * attributes.size());
for (size_t i = 0; i < attributes.size(); ++i) {
header.custom_attributes[i] = attributes[i];
}
}
} exr;
exr.image.images = reinterpret_cast<unsigned char**>(imagePtrs.data());
exr.image.width = static_cast<int>(width);
exr.image.height = static_cast<int>(height);
exr.image.num_channels = static_cast<int>(numChannels);
exr.header.num_channels = static_cast<int>(numChannels);
exr.header.channels = (EXRChannelInfo*) malloc(sizeof(EXRChannelInfo) * exr.header.num_channels);
// TODO: Question: Should we use a compression for exr outputs?
exr.header.compression_type = TINYEXR_COMPRESSIONTYPE_NONE;
for (uint32_t c = 0; c < numChannels; c++) {
const auto& channel = channels[c];
strncpy(exr.header.channels[c].name, channel.name, 255);
imagePtrs[c] = images[c].data();
}
exr.header.pixel_types = (int *)malloc(sizeof(int) * exr.header.num_channels);
exr.header.requested_pixel_types = (int *)malloc(sizeof(int) * exr.header.num_channels);
for (int i = 0; i < exr.header.num_channels; i++) {
// pixel type of the input image
exr.header.pixel_types[i] = (pixelTypes[i] == TINYEXR_PIXELTYPE_UINT) ? TINYEXR_PIXELTYPE_UINT : TINYEXR_PIXELTYPE_FLOAT;
// pixel type of the output image to be stored in .EXR file
exr.header.requested_pixel_types[i] = pixelTypes[i];
}
// Output primaries as chromaticities
Primaries primaries;
bool foundPrimaries = getPrimaries(format.primaries(), &primaries);
if (foundPrimaries) {
EXRAttribute chromaticities{};
strncpy(chromaticities.name, "chromaticities", sizeof(chromaticities.name));
strncpy(chromaticities.type, "chromaticities", sizeof(chromaticities.type));
chromaticities.size = sizeof(primaries);
chromaticities.value = reinterpret_cast<unsigned char*>(&primaries);
exr.attributes.push_back(chromaticities);
}
exr.addAttributesToHeader();
std::size_t fileDataSize = SaveEXRImageToMemory(&exr.image, &exr.header, &exr.fileData, &exr.err);
if (fileDataSize == 0)
fatal(rc::IO_FAILURE, "EXR Encoder error: {}.", exr.err);
OutputStream file(filepath, *this);
file.write(reinterpret_cast<const char*>(exr.fileData), fileDataSize, *this);
}
void CommandExtract::saveImageFile(
std::string filepath, bool appendExtension,
const char* data, std::size_t size,
VkFormat vkFormat, const FormatDescriptor& format, uint32_t width, uint32_t height) {
switch (vkFormat) {
case VK_FORMAT_A8_UNORM_KHR: [[fallthrough]];
case VK_FORMAT_R8_UNORM: [[fallthrough]];
case VK_FORMAT_R8_SRGB: [[fallthrough]];
case VK_FORMAT_R8G8_UNORM: [[fallthrough]];
case VK_FORMAT_R8G8_SRGB: [[fallthrough]];
case VK_FORMAT_R8G8B8_UNORM: [[fallthrough]];
case VK_FORMAT_R8G8B8_SRGB: [[fallthrough]];
case VK_FORMAT_B8G8R8_UNORM: [[fallthrough]];
case VK_FORMAT_B8G8R8_SRGB: [[fallthrough]];
case VK_FORMAT_A8B8G8R8_UNORM_PACK32: [[fallthrough]];
case VK_FORMAT_A8B8G8R8_SRGB_PACK32: [[fallthrough]];
case VK_FORMAT_R8G8B8A8_UNORM: [[fallthrough]];
case VK_FORMAT_R8G8B8A8_SRGB: [[fallthrough]];
case VK_FORMAT_B8G8R8A8_UNORM: [[fallthrough]];
case VK_FORMAT_B8G8R8A8_SRGB:
savePNG(std::move(filepath), appendExtension, vkFormat, format, width, height, data, size);
break;
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 decode will recurse into this function with the uncompressed data and format
decodeAndSaveASTC(std::move(filepath), appendExtension, vkFormat, format, width, height, data, size);
break;
case VK_FORMAT_R4G4_UNORM_PACK8: [[fallthrough]];
case VK_FORMAT_R5G6B5_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_B5G6R5_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_R4G4B4A4_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_B4G4R4A4_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_R5G5B5A1_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_B5G5R5A1_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_A1R5G5B5_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_A1B5G5R5_UNORM_PACK16_KHR: [[fallthrough]];
case VK_FORMAT_A4R4G4B4_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_A4B4G4R4_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_R10X6_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_R10X6G10X6_UNORM_2PACK16: [[fallthrough]];
case VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16: [[fallthrough]];
case VK_FORMAT_R12X4_UNORM_PACK16: [[fallthrough]];
case VK_FORMAT_R12X4G12X4_UNORM_2PACK16: [[fallthrough]];
case VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16: [[fallthrough]];
case VK_FORMAT_R16_UNORM: [[fallthrough]];
case VK_FORMAT_R16G16_UNORM: [[fallthrough]];
case VK_FORMAT_R16G16B16_UNORM: [[fallthrough]];
case VK_FORMAT_R16G16B16A16_UNORM: [[fallthrough]];
case VK_FORMAT_A2R10G10B10_UNORM_PACK32: [[fallthrough]];
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
savePNG(std::move(filepath), appendExtension, vkFormat, format, width, height, data, size);
break;
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:
// Unpack and save 4:2:2 formats as UNORM8, UNORM10X6, UNORM12X4 or UNORM16 formats
unpackAndSave422(std::move(filepath), appendExtension, vkFormat, format, width, height, data, size);
break;
// EXR
case VK_FORMAT_R8_UINT: [[fallthrough]];
case VK_FORMAT_R8_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_R16_UINT: [[fallthrough]];
case VK_FORMAT_R16_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_FLOAT, data, size);
break;
case VK_FORMAT_R32_UINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_UINT, data, size);
break;
case VK_FORMAT_R8G8_UINT: [[fallthrough]];
case VK_FORMAT_R8G8_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_R16G16_UINT: [[fallthrough]];
case VK_FORMAT_R16G16_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_FLOAT, data, size);
break;
case VK_FORMAT_R32G32_UINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_UINT, data, size);
break;
case VK_FORMAT_R8G8B8_UINT: [[fallthrough]];
case VK_FORMAT_R8G8B8_SINT: [[fallthrough]];
case VK_FORMAT_B8G8R8_UINT: [[fallthrough]];
case VK_FORMAT_B8G8R8_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_R16G16B16_UINT: [[fallthrough]];
case VK_FORMAT_R16G16B16_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_FLOAT, data, size);
break;
case VK_FORMAT_R32G32B32_UINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_UINT, data, size);
break;
case VK_FORMAT_R8G8B8A8_UINT: [[fallthrough]];
case VK_FORMAT_R8G8B8A8_SINT: [[fallthrough]];
case VK_FORMAT_B8G8R8A8_UINT: [[fallthrough]];
case VK_FORMAT_B8G8R8A8_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_R16G16B16A16_UINT: [[fallthrough]];
case VK_FORMAT_R16G16B16A16_SINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_FLOAT, data, size);
break;
case VK_FORMAT_R32G32B32A32_UINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_UINT, data, size);
break;
case VK_FORMAT_A2R10G10B10_UINT_PACK32: [[fallthrough]];
case VK_FORMAT_A2R10G10B10_SINT_PACK32: [[fallthrough]];
case VK_FORMAT_A2B10G10R10_UINT_PACK32: [[fallthrough]];
case VK_FORMAT_A2B10G10R10_SINT_PACK32:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_R16_SFLOAT: [[fallthrough]];
case VK_FORMAT_R16G16_SFLOAT: [[fallthrough]];
case VK_FORMAT_R16G16B16_SFLOAT: [[fallthrough]];
case VK_FORMAT_R16G16B16A16_SFLOAT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_R32_SFLOAT: [[fallthrough]];
case VK_FORMAT_R32G32_SFLOAT: [[fallthrough]];
case VK_FORMAT_R32G32B32_SFLOAT: [[fallthrough]];
case VK_FORMAT_R32G32B32A32_SFLOAT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_FLOAT, data, size);
break;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_D16_UNORM:
savePNG(std::move(filepath), appendExtension, vkFormat, format, width, height, data, size);
break;
case VK_FORMAT_X8_D24_UNORM_PACK32: [[fallthrough]];
case VK_FORMAT_D32_SFLOAT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_FLOAT, data, size);
break;
case VK_FORMAT_S8_UINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, TINYEXR_PIXELTYPE_HALF, data, size);
break;
case VK_FORMAT_D16_UNORM_S8_UINT: [[fallthrough]];
case VK_FORMAT_D24_UNORM_S8_UINT: [[fallthrough]];
case VK_FORMAT_D32_SFLOAT_S8_UINT:
saveEXR(std::move(filepath), appendExtension, vkFormat, format, width, height, {TINYEXR_PIXELTYPE_FLOAT, TINYEXR_PIXELTYPE_HALF}, data, size);
break;
default:
fatal(rc::INVALID_FILE, "Requested format conversion from {} is not supported.", toString(vkFormat));
}
}
} // namespace ktx
KTX_COMMAND_ENTRY_POINT(ktxExtract, ktx::CommandExtract)