Files
2026-06-14 19:09:18 +01:00

462 lines
20 KiB
C++

// Copyright 2022-2023 The Khronos Group Inc.
// Copyright 2022-2023 RasterGrid Kft.
// SPDX-License-Identifier: Apache-2.0
#include "ktx.h"
#include "ktxint.h"
#include "texture2.h"
#include "vkformat_enum.h"
#include "platform_utils.h"
#include "astc-encoder/Source/astcenc.h"
#include <cassert>
#include <fstream>
#include <iostream>
#include <string_view>
#include <vector>
#include <fmt/os.h>
#include <fmt/ostream.h>
#include <fmt/printf.h>
template <typename T>
[[nodiscard]] constexpr inline T ceil_div(const T x, const T y) noexcept {
assert(y != 0);
return (x + y - 1) / y;
}
// C++20 - std::bit_cast
template <class To, class From>
[[nodiscard]] constexpr inline To bit_cast(const From& src) noexcept {
static_assert(sizeof(To) == sizeof(From));
static_assert(std::is_trivially_copyable_v<From>);
static_assert(std::is_trivially_copyable_v<To>);
static_assert(std::is_trivially_constructible_v<To>);
To dst;
std::memcpy(&dst, &src, sizeof(To));
return dst;
}
[[nodiscard]] constexpr inline bool isFormatAstc(VkFormat format) noexcept {
switch (format) {
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: [[fallthrough]];
case VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK: [[fallthrough]];
case VK_FORMAT_ASTC_3x3x3_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_3x3x3_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_3x3x3_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x3x3_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x3x3_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x3x3_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x4x3_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x4x3_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x4x3_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x4x4_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x4x4_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_4x4x4_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x4x4_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x4x4_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x4x4_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x5x4_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x5x4_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x5x4_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x5x5_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x5x5_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_5x5x5_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x5x5_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x5x5_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x5x5_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x6x5_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x6x5_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x6x5_SFLOAT_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x6x6_UNORM_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x6x6_SRGB_BLOCK_EXT: [[fallthrough]];
case VK_FORMAT_ASTC_6x6x6_SFLOAT_BLOCK_EXT:
return true;
default:
return false;
}
}
// -------------------------------------------------------------------------------------------------
int EXIT_CODE_ERROR = 2;
int EXIT_CODE_MISMATCH = 1;
int EXIT_CODE_MATCH = 0;
template <typename... Args>
void error(int return_code, Args&&... args) {
fmt::print(std::cerr, std::forward<Args>(args)...);
std::exit(return_code);
}
[[nodiscard]] inline std::string errnoMessage() {
return std::make_error_code(static_cast<std::errc>(errno)).message();
}
struct Texture {
std::string filepath;
std::vector<std::byte> rawData;
KTX_header2 header;
std::vector<ktxLevelIndexEntry> levelIndices;
const std::byte* levelIndexData = nullptr;
size_t levelIndexSize = 0;
const std::byte* dfdData = nullptr;
size_t dfdSize = 0;
const std::byte* kvdData = nullptr;
size_t kvdSize = 0;
const std::byte* sgdData = nullptr;
size_t sgdSize = 0;
ktxTexture2* handle = nullptr;
bool transcoded = false;
public:
explicit Texture(std::string filepath) :
filepath(filepath) {
std::memset(&header, 0, sizeof(header));
loadFile();
loadKTX();
loadMetadata();
}
~Texture() {
std::free(handle);
}
void loadFile();
void loadKTX();
void loadMetadata();
inline ktxTexture2* operator->() const {
return handle;
}
};
void Texture::loadFile() {
auto file = std::ifstream(DecodeUTF8Path(filepath).c_str(), std::ios::binary | std::ios::in | std::ios::ate);
if (!file)
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": Failed to open file: {}\n", filepath, errnoMessage());
const auto fileSize = file.tellg();
file.seekg(0);
if (file.fail())
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": Failed to seek file: {}\n", filepath, errnoMessage());
rawData.resize(fileSize);
file.read(reinterpret_cast<char*>(rawData.data()), fileSize);
if (file.fail())
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": Failed to read file: {}\n", filepath, errnoMessage());
}
void Texture::loadKTX() {
KTX_error_code ec = KTX_SUCCESS;
ec = ktxTexture2_CreateFromMemory(
reinterpret_cast<const ktx_uint8_t*>(rawData.data()),
rawData.size(),
KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT,
&handle);
if (ec != KTX_SUCCESS)
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": ktxTexture2_CreateFromNamedFile: {}\n", filepath, ktxErrorString(ec));
if (ktxTexture2_NeedsTranscoding(handle)) {
ec = ktxTexture2_TranscodeBasis(handle, KTX_TTF_RGBA32, 0);
if (ec != KTX_SUCCESS)
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": ktxTexture2_TranscodeBasis: {}\n", filepath, ktxErrorString(ec));
transcoded = true;
}
}
void Texture::loadMetadata() {
const auto headerData = rawData.data();
const auto headerSize = sizeof(KTX_header2);
std::memcpy(&header, headerData, headerSize);
const auto numLevels = std::max(header.levelCount, 1u);
levelIndexData = rawData.data() + sizeof(KTX_header2);
levelIndexSize = sizeof(ktxLevelIndexEntry) * numLevels;
levelIndices.resize(numLevels);
std::memcpy(levelIndices.data(), levelIndexData, levelIndexSize);
if (header.dataFormatDescriptor.byteLength != 0) {
dfdData = rawData.data() + header.dataFormatDescriptor.byteOffset;
dfdSize = header.dataFormatDescriptor.byteLength;
}
if (header.keyValueData.byteLength != 0) {
kvdData = rawData.data() + header.keyValueData.byteOffset;
kvdSize = header.keyValueData.byteLength;
}
if (header.supercompressionGlobalData.byteLength != 0) {
sgdData = rawData.data() + header.dataFormatDescriptor.byteOffset;
sgdSize = header.dataFormatDescriptor.byteLength;
}
}
// -------------------------------------------------------------------------------------------------
struct CompareResult {
bool match = true;
float difference = 0.f;
std::size_t elementIndex = 0;
std::size_t byteOffset = 0;
};
CompareResult compareUnorm8(const char* rawLhs, const char* rawRhs, std::size_t rawSize, float tolerance) {
const auto* lhs = reinterpret_cast<const uint8_t*>(rawLhs);
const auto* rhs = reinterpret_cast<const uint8_t*>(rawRhs);
const auto element_size = sizeof(uint8_t);
const auto count = rawSize / element_size;
for (std::size_t i = 0; i < count; ++i) {
const auto diff = std::abs(static_cast<float>(lhs[i]) / 255.f - static_cast<float>(rhs[i]) / 255.f);
if (diff > tolerance)
return CompareResult{false, diff, i, i * element_size};
}
return CompareResult{};
}
CompareResult compareSFloat32(const char* rawLhs, const char* rawRhs, std::size_t rawSize, float tolerance) {
const auto* lhs = reinterpret_cast<const float*>(rawLhs);
const auto* rhs = reinterpret_cast<const float*>(rawRhs);
const auto element_size = sizeof(float);
const auto count = rawSize / element_size;
for (std::size_t i = 0; i < count; ++i) {
const auto diff = std::abs(lhs[i] - rhs[i]);
if (diff > tolerance)
return CompareResult{false, diff, i, i * element_size};
}
return CompareResult{};
}
auto decodeASTC(const char* compressedData, std::size_t compressedSize, uint32_t width, uint32_t height,
const std::string& filepath, bool isFormatSRGB, uint32_t blockSizeX, uint32_t blockSizeY, uint32_t blockSizeZ) {
const auto threadCount = 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 ? 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)
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": astcenc_config_init: {}\n", filepath, 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)
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": astcenc_context_alloc: {}\n", filepath, 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);
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)
error(EXIT_CODE_ERROR, "ktxdiff error \"{}\": astcenc_decompress_image: {}\n", filepath, astcenc_get_error_string(ec));
astcenc_decompress_reset(context);
struct Result {
std::unique_ptr<uint8_t[]> data;
std::size_t size;
};
return Result{std::move(uncompressedBuffer), uncompressedSize};
}
CompareResult compareAstc(const char* lhs, const char* rhs, std::size_t size, uint32_t width, uint32_t height,
const std::string& filepathLhs, const std::string& filepathRhs,
bool isFormatSRGB, uint32_t blockSizeX, uint32_t blockSizeY, uint32_t blockSizeZ,
float tolerance) {
const auto uncompressedLhs = decodeASTC(lhs, size, width, height, filepathLhs, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ);
const auto uncompressedRhs = decodeASTC(rhs, size, width, height, filepathRhs, isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ);
return compareUnorm8(
reinterpret_cast<const char*>(uncompressedLhs.data.get()),
reinterpret_cast<const char*>(uncompressedRhs.data.get()),
uncompressedLhs.size,
tolerance);
}
bool compare(Texture& lhs, Texture& rhs, float tolerance) {
const auto vkFormat = static_cast<VkFormat>(lhs.header.vkFormat);
const auto* bdfd = reinterpret_cast<const uint32_t*>(lhs.dfdData) + 1;
const auto componentCount = KHR_DFDSAMPLECOUNT(bdfd);
const auto texelBlockDimension0 = static_cast<uint8_t>(KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION0));
const auto texelBlockDimension1 = static_cast<uint8_t>(KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION1));
const auto texelBlockDimension2 = static_cast<uint8_t>(KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION2));
const auto blockSizeX = texelBlockDimension0 + 1u;
const auto blockSizeY = texelBlockDimension1 + 1u;
const auto blockSizeZ = texelBlockDimension2 + 1u;
const bool isFormatSRGB = KHR_DFDVAL(bdfd, TRANSFER) == KHR_DF_TRANSFER_SRGB;
const bool isSigned = (KHR_DFDSVAL(bdfd, 0, QUALIFIERS) & KHR_DF_SAMPLE_DATATYPE_SIGNED) != 0;
const bool isFloat = (KHR_DFDSVAL(bdfd, 0, QUALIFIERS) & KHR_DF_SAMPLE_DATATYPE_FLOAT) != 0;
const bool isNormalized = KHR_DFDSVAL(bdfd, 0, SAMPLEUPPER) == (isFloat ? bit_cast<uint32_t>(1.0f) : 1u);
const bool is32Bit = KHR_DFDSVAL(bdfd, 0, BITLENGTH) + 1 == 32;
const bool is8Bit = KHR_DFDSVAL(bdfd, 0, BITLENGTH) + 1 == 8;
const bool isFormatSFloat32 = isSigned && isFloat && is32Bit && vkFormat != VK_FORMAT_D32_SFLOAT_S8_UINT;
const bool isFormatUNORM8 = !isSigned && !isFloat && is8Bit && isNormalized;
const auto mismatch = [&](auto&&... args) {
fmt::print("ktxdiff: ");
fmt::print(std::forward<decltype(args)>(args)...);
fmt::print(" between\n");
fmt::print(" Expected: {} and\n", lhs.filepath);
fmt::print(" Received: {}\n", rhs.filepath);
return false;
};
if (lhs.transcoded) {
// For encoded images the compressed data sizes can differ.
// Skip the related checks for header.supercompressionGlobalData and levelIndex
if (std::memcmp(&lhs.header, &rhs.header, sizeof(lhs.header) - sizeof(ktxIndexEntry64)) != 0)
return mismatch("Mismatching header");
} else {
if (std::memcmp(&lhs.header, &rhs.header, sizeof(lhs.header)) != 0)
return mismatch("Mismatching header");
if (lhs.levelIndexSize != rhs.levelIndexSize)
return mismatch("Mismatching levelIndices");
for (uint32_t i = 0; i < lhs.levelIndices.size(); ++i)
// Offsets and (compressed) sizes can differ, but uncompressedByteLength must match
if (lhs.levelIndices[i].uncompressedByteLength != rhs.levelIndices[i].uncompressedByteLength)
return mismatch("Mismatching levelIndices[{}].uncompressedByteLength", i);
}
if (lhs.dfdSize != rhs.dfdSize || std::memcmp(lhs.dfdData, rhs.dfdData, lhs.dfdSize) != 0)
return mismatch("Mismatching DFD");
if (lhs.kvdSize != rhs.kvdSize || std::memcmp(lhs.kvdData, rhs.kvdData, lhs.kvdSize) != 0)
return mismatch("Mismatching KVD");
if (!lhs.transcoded)
if (lhs.sgdSize != rhs.sgdSize || std::memcmp(lhs.sgdData, rhs.sgdData, lhs.sgdSize) != 0)
return mismatch("Mismatching SGD");
// If the tolerance is 1 or above accept every image data as matching
if (tolerance >= 1.0f)
return true;
for (uint32_t levelIndex = 0; levelIndex < lhs->numLevels; ++levelIndex) {
const auto imageSize = ktxTexture_GetImageSize(ktxTexture(lhs.handle), levelIndex);
const auto imageWidth = std::max(1u, lhs->baseWidth >> levelIndex);
const auto imageHeight = std::max(1u, lhs->baseHeight >> levelIndex);
const auto imageDepth = std::max(1u, lhs->baseDepth >> levelIndex);
for (uint32_t faceIndex = 0; faceIndex < lhs->numFaces; ++faceIndex) {
for (uint32_t layerIndex = 0; layerIndex < lhs->numLayers; ++layerIndex) {
for (uint32_t depthIndex = 0; depthIndex < ceil_div(imageDepth, blockSizeZ); ++depthIndex) {
ktx_size_t imageOffset;
ktxTexture2_GetImageOffset(lhs.handle, levelIndex, layerIndex, faceIndex + depthIndex, &imageOffset);
const char* imageDataLhs = reinterpret_cast<const char*>(lhs->pData) + imageOffset;
const char* imageDataRhs = reinterpret_cast<const char*>(rhs->pData) + imageOffset;
CompareResult result;
if (lhs.transcoded || isFormatUNORM8) {
result = compareUnorm8(imageDataLhs, imageDataRhs, imageSize, tolerance);
} else if (isFormatAstc(vkFormat)) {
result = compareAstc(imageDataLhs, imageDataRhs, imageSize, imageWidth, imageHeight,
lhs.filepath, rhs.filepath,
isFormatSRGB, blockSizeX, blockSizeY, blockSizeZ,
tolerance);
} else if (isFormatSFloat32) {
result = compareSFloat32(imageDataLhs, imageDataRhs, imageSize, tolerance);
} else {
for (std::size_t i = 0; i < imageSize; ++i) {
if (imageDataLhs[i] != imageDataRhs[i])
return mismatch("Mismatching image data: level {}, face {}, layer {}, depth {}, image byte {}",
levelIndex, faceIndex, layerIndex, depthIndex, i);
}
}
if (!result.match) {
return mismatch("Mismatching image data (diff: {}): level {}, face {}, layer {}, depth {}, pixel {}, component {}",
result.difference, levelIndex, faceIndex, layerIndex, depthIndex,
result.elementIndex / componentCount, result.elementIndex % componentCount);
}
}
}
}
}
return true;
}
/// EXIT CODES:
/// 0 - Matching files
/// 1 - Mismatching files
/// 2 - Error while loading, decoding or processing an input file
int main(int argc, char* argv[]) {
InitUTF8CLI(argc, argv);
if (argc < 3) {
fmt::print("Missing input file arguments\n");
fmt::print("Usage: ktxdiff <expected-ktx2> <received-ktx2> [tolerance]\n");
return EXIT_FAILURE;
}
const float tolerance = argc > 3 ? std::stof(argv[3]) : 0.05f;
Texture lhs(argv[1]);
Texture rhs(argv[2]);
const auto match = compare(lhs, rhs, tolerance);
return match ? 0 : 1;
}