blob: 9154ab60e9cf0042e84c71569d3a0ba989bbb21d [file]
/*
* Copyright 2026 Rive
*/
// Parse-only KTX2 reader for Rive runtime.
//
// Extracts vkFormat, logical pixel dims, and mip-level block payloads from a
// KTX2 container. Does NOT decompress — block data is returned verbatim so
// the renderer can hand it straight to the GPU's compressed texture upload
// path. Today only BC7 (UNORM / SRGB) is recognized; other vkFormat values
// are rejected.
//
// Spec: https://registry.khronos.org/KTX/specs/2.0/ktxspec.v2.html
#include "rive/decoders/decode_ktx2.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
namespace rive
{
namespace
{
constexpr uint8_t kKtx2Identifier[12] = {
0xAB,
0x4B,
0x54,
0x58,
0x20,
0x32,
0x30,
0xBB,
0x0D,
0x0A,
0x1A,
0x0A,
};
constexpr uint32_t VK_FORMAT_BC7_UNORM_BLOCK = 145;
constexpr uint32_t VK_FORMAT_BC7_SRGB_BLOCK = 146;
constexpr uint32_t kSupercompressionNone = 0;
#pragma pack(push, 1)
struct Ktx2Header
{
uint32_t vkFormat;
uint32_t typeSize;
uint32_t pixelWidth;
uint32_t pixelHeight;
uint32_t pixelDepth;
uint32_t layerCount;
uint32_t faceCount;
uint32_t levelCount;
uint32_t supercompressionScheme;
uint32_t dfdByteOffset;
uint32_t dfdByteLength;
uint32_t kvdByteOffset;
uint32_t kvdByteLength;
uint64_t sgdByteOffset;
uint64_t sgdByteLength;
};
struct Ktx2LevelIndex
{
uint64_t byteOffset;
uint64_t byteLength;
uint64_t uncompressedByteLength;
};
#pragma pack(pop)
static_assert(sizeof(Ktx2Header) == 68, "KTX2 header must be 68 bytes");
static_assert(sizeof(Ktx2LevelIndex) == 24,
"KTX2 level index entry must be 24 bytes");
// Defensive caps.
constexpr uint32_t kMaxDimension = 16384;
constexpr uint32_t kMaxLevels = 16;
constexpr uint32_t kBc7BlockBytes = 16;
// Expected block-grid byte length for a BC7 mip level at the given logical
// pixel dimensions. BC7 = 4x4 blocks, 16 bytes/block.
inline uint64_t expectedBc7Bytes(uint32_t pixelWidth, uint32_t pixelHeight)
{
const uint64_t blocksX = (pixelWidth + 3u) / 4u;
const uint64_t blocksY = (pixelHeight + 3u) / 4u;
return blocksX * blocksY * kBc7BlockBytes;
}
} // namespace
bool DecodeKtx2(const uint8_t* bytes, size_t byteCount, Ktx2DecodeResult& out)
{
if (byteCount < sizeof(kKtx2Identifier) + sizeof(Ktx2Header))
{
std::fprintf(stderr, "DecodeKtx2: file too small\n");
return false;
}
if (std::memcmp(bytes, kKtx2Identifier, sizeof(kKtx2Identifier)) != 0)
{
std::fprintf(stderr, "DecodeKtx2: bad magic\n");
return false;
}
Ktx2Header header;
std::memcpy(&header, bytes + sizeof(kKtx2Identifier), sizeof(header));
if (header.vkFormat != VK_FORMAT_BC7_UNORM_BLOCK &&
header.vkFormat != VK_FORMAT_BC7_SRGB_BLOCK)
{
std::fprintf(stderr,
"DecodeKtx2: unsupported vkFormat %u (only BC7 wired)\n",
header.vkFormat);
return false;
}
if (header.supercompressionScheme != kSupercompressionNone)
{
std::fprintf(stderr,
"DecodeKtx2: supercompressionScheme %u not supported\n",
header.supercompressionScheme);
return false;
}
if (header.faceCount != 1 || header.layerCount != 0)
{
std::fprintf(
stderr,
"DecodeKtx2: cubemaps/arrays not supported (faces=%u layers=%u)\n",
header.faceCount,
header.layerCount);
return false;
}
if (header.pixelWidth == 0 || header.pixelWidth > kMaxDimension ||
header.pixelHeight == 0 || header.pixelHeight > kMaxDimension)
{
std::fprintf(stderr,
"DecodeKtx2: dimensions out of range (%ux%u)\n",
header.pixelWidth,
header.pixelHeight);
return false;
}
const uint32_t levelCount = header.levelCount == 0 ? 1u : header.levelCount;
if (levelCount > kMaxLevels)
{
std::fprintf(stderr,
"DecodeKtx2: levelCount %u exceeds cap %u\n",
levelCount,
kMaxLevels);
return false;
}
const size_t levelIndexOffset =
sizeof(kKtx2Identifier) + sizeof(Ktx2Header);
const size_t levelIndexBytes =
static_cast<size_t>(levelCount) * sizeof(Ktx2LevelIndex);
if (byteCount < levelIndexOffset + levelIndexBytes)
{
std::fprintf(stderr, "DecodeKtx2: truncated level index\n");
return false;
}
// Read level entries (level 0 first in the index = largest mip).
std::vector<Ktx2LevelIndex> entries(levelCount);
std::memcpy(entries.data(), bytes + levelIndexOffset, levelIndexBytes);
// Validate each level + sum total payload size for the output buffer.
uint64_t totalBytes = 0;
for (uint32_t i = 0; i < levelCount; ++i)
{
const Ktx2LevelIndex& e = entries[i];
const uint32_t logW = std::max<uint32_t>(1u, header.pixelWidth >> i);
const uint32_t logH = std::max<uint32_t>(1u, header.pixelHeight >> i);
const uint64_t expected = expectedBc7Bytes(logW, logH);
if (e.byteLength != expected)
{
std::fprintf(
stderr,
"DecodeKtx2: level %u byteLength %llu != block grid %llu "
"for %ux%u\n",
i,
static_cast<unsigned long long>(e.byteLength),
static_cast<unsigned long long>(expected),
logW,
logH);
return false;
}
if (e.byteOffset > byteCount || e.byteLength > byteCount - e.byteOffset)
{
std::fprintf(stderr, "DecodeKtx2: level %u out of buffer\n", i);
return false;
}
totalBytes += e.byteLength;
}
// Concatenate level 0 .. N-1 into one contiguous buffer (largest first).
out.format = header.vkFormat == VK_FORMAT_BC7_SRGB_BLOCK
? GPUTextureFormat::bc7
: GPUTextureFormat::bc7;
out.pixelWidth = header.pixelWidth;
out.pixelHeight = header.pixelHeight;
out.levelCount = levelCount;
out.blocks.resize(static_cast<size_t>(totalBytes));
size_t writeOffset = 0;
for (uint32_t i = 0; i < levelCount; ++i)
{
const Ktx2LevelIndex& e = entries[i];
std::memcpy(out.blocks.data() + writeOffset,
bytes + e.byteOffset,
static_cast<size_t>(e.byteLength));
writeOffset += static_cast<size_t>(e.byteLength);
}
return true;
}
} // namespace rive