diff --git a/src/core/ktx2_loader.cpp b/src/core/ktx2_loader.cpp new file mode 100644 index 0000000..6acea40 --- /dev/null +++ b/src/core/ktx2_loader.cpp @@ -0,0 +1,213 @@ +#include "ktx2_loader.h" + +#include +#include +#include + +namespace { + +struct KTX2Header { + uint8_t magic[12]; + 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; + uint64_t dfdByteOffset; + uint32_t dfdByteLength; + uint64_t kvdByteOffset; + uint32_t kvdByteLength; + uint64_t sgdByteOffset; + uint64_t sgdByteLength; +}; + +struct KTX2LevelIndexEntry { + uint64_t byteOffset; + uint64_t byteLength; + uint64_t uncompressedByteLength; +}; + +constexpr uint8_t KTX2_MAGIC[12] = { + 0xAB,'K','T','X',' ', '2','0', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A +}; + +template +static inline bool read_into(const uint8_t* base, size_t size, size_t off, T& out) +{ + if (off + sizeof(T) > size) return false; + std::memcpy(&out, base + off, sizeof(T)); + return true; +} + +} // namespace + +static inline uint32_t bc_block_bytes(VkFormat fmt) +{ + switch (fmt) + { + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: + case VK_FORMAT_BC1_RGB_SRGB_BLOCK: + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: + case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: + case VK_FORMAT_BC4_UNORM_BLOCK: + case VK_FORMAT_BC4_SNORM_BLOCK: + return 8u; + case VK_FORMAT_BC2_UNORM_BLOCK: + case VK_FORMAT_BC2_SRGB_BLOCK: + case VK_FORMAT_BC3_UNORM_BLOCK: + case VK_FORMAT_BC3_SRGB_BLOCK: + case VK_FORMAT_BC5_UNORM_BLOCK: + case VK_FORMAT_BC5_SNORM_BLOCK: + case VK_FORMAT_BC6H_UFLOAT_BLOCK: + case VK_FORMAT_BC6H_SFLOAT_BLOCK: + case VK_FORMAT_BC7_UNORM_BLOCK: + case VK_FORMAT_BC7_SRGB_BLOCK: + return 16u; + default: return 0u; + } +} + +bool parse_ktx2(const uint8_t* bytes, size_t size, KTX2Image& out, std::string* err) +{ + if (!bytes || size < sizeof(KTX2Header)) + { + if (err) *err = "KTX2: buffer too small"; + return false; + } + + KTX2Header hdr{}; + if (!read_into(bytes, size, 0, hdr)) + { + if (err) *err = "KTX2: failed to read header"; + return false; + } + if (std::memcmp(hdr.magic, KTX2_MAGIC, sizeof(KTX2_MAGIC)) != 0) + { + if (err) *err = "KTX2: bad magic"; + return false; + } + + if (hdr.levelCount == 0 || hdr.pixelWidth == 0 || hdr.pixelHeight == 0) + { + if (err) *err = "KTX2: invalid dimensions or levels"; + return false; + } + if (hdr.layerCount > 1 || hdr.faceCount != 1) + { + if (err) *err = "KTX2: only 2D, single-face, single-layer supported"; + return false; + } + if (hdr.supercompressionScheme != 0) + { + if (err) *err = "KTX2: supercompressed payloads not supported"; + return false; + } + if (hdr.vkFormat == 0) + { + if (err) *err = "KTX2: vkFormat undefined (expected pre-transcoded BCn)"; + return false; + } + + // Level index immediately follows header in KTX2 layout. + const size_t levelIndexSize = sizeof(KTX2LevelIndexEntry) * static_cast(hdr.levelCount); + auto align8 = [](uint64_t x) { return (x + 7ull) & ~7ull; }; + + // Per KTX2 spec, the Level Index immediately follows the fixed-size 80-byte header. + size_t levelIndexOffset = sizeof(KTX2Header); + if (levelIndexOffset + levelIndexSize > size) + { + if (err) *err = "KTX2: truncated level index"; + return false; + } + + std::vector levels(hdr.levelCount); + std::memcpy(levels.data(), bytes + levelIndexOffset, levelIndexSize); + + // Debug header/offsets when requested via env (VE_TEX_DEBUG=1) + if (const char* dbg = std::getenv("VE_TEX_DEBUG"); dbg && dbg[0] == '1') + { + fmt::println("[KTX2] hdr: fmt={}, size={}x{} levels={} dfdOff={} dfdLen={} kvdOff={} kvdLen={} sgdOff={} sgdLen={} liOff={}", + (unsigned)hdr.vkFormat, hdr.pixelWidth, hdr.pixelHeight, hdr.levelCount, + (unsigned long long)hdr.dfdByteOffset, (unsigned)hdr.dfdByteLength, + (unsigned long long)hdr.kvdByteOffset, (unsigned)hdr.kvdByteLength, + (unsigned long long)hdr.sgdByteOffset, (unsigned long long)hdr.sgdByteLength, + (unsigned long long)levelIndexOffset); + for (uint32_t i = 0; i < hdr.levelCount; ++i) + { + fmt::println("[KTX2] LI[{}]: offRel={} len={} uncomp={}", i, + (unsigned long long)levels[i].byteOffset, + (unsigned long long)levels[i].byteLength, + (unsigned long long)levels[i].uncompressedByteLength); + } + } + + // Compute dataStart per spec: after level index and any optional blocks, 8-byte aligned. + uint64_t afterIndex = align8(static_cast(levelIndexOffset + levelIndexSize)); + uint64_t dfdEnd = static_cast(hdr.dfdByteOffset) + static_cast(hdr.dfdByteLength); + uint64_t kvdEnd = static_cast(hdr.kvdByteOffset) + static_cast(hdr.kvdByteLength); + uint64_t sgdEnd = static_cast(hdr.sgdByteOffset) + static_cast(hdr.sgdByteLength); + uint64_t dataStart = align8(std::max({ afterIndex, dfdEnd, kvdEnd, sgdEnd })); + if (dataStart == 0 || dataStart > size) + { + if (err) *err = "KTX2: could not locate level data start"; + return false; + } + + out = {}; + out.format = static_cast(hdr.vkFormat); + out.width = hdr.pixelWidth; + out.height = hdr.pixelHeight; + out.mipLevels = hdr.levelCount; + out.faceCount = hdr.faceCount; + out.layerCount = hdr.layerCount; + out.supercompression = hdr.supercompressionScheme; + out.data.assign(bytes, bytes + size); // retain backing store for staging copies + out.levels.resize(hdr.levelCount); + // Map entries to mip levels: assign largest byteLength to mip 0, next to mip 1, etc. + std::vector order(hdr.levelCount); + for (uint32_t i = 0; i < hdr.levelCount; ++i) order[i] = i; + std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b){ return levels[a].byteLength > levels[b].byteLength; }); + + out.levels.resize(hdr.levelCount); + const uint32_t blockBytes = bc_block_bytes(static_cast(hdr.vkFormat)); + for (uint32_t mip = 0; mip < hdr.levelCount; ++mip) + { + const auto &li = levels[ order[mip] ]; + const uint32_t w = std::max(1u, hdr.pixelWidth >> mip); + const uint32_t h = std::max(1u, hdr.pixelHeight >> mip); + + if (blockBytes) + { + uint64_t bx = (w + 3u) / 4u; + uint64_t by = (h + 3u) / 4u; + uint64_t expected = bx * by * blockBytes; + if (li.byteLength < expected) + { + if (err) + { + char buf[256]; + snprintf(buf, sizeof(buf), + "KTX2: level length smaller than expected footprint (mip=%u fmt=%u w=%u h=%u blocks=%llux%llu blockBytes=%u expected=%llu got=%llu)", + mip, (unsigned)hdr.vkFormat, w, h, + (unsigned long long)bx, (unsigned long long)by, + blockBytes, (unsigned long long)expected, (unsigned long long)li.byteLength); + *err = buf; + } + return false; + } + } + + uint64_t absOff = dataStart + li.byteOffset; + if (absOff + li.byteLength > size) + { + if (err) *err = "KTX2: level range out of bounds"; + return false; + } + out.levels[mip] = KTX2LevelInfo{ absOff, li.byteLength, w, h }; + } + return true; +} diff --git a/src/core/ktx2_loader.h b/src/core/ktx2_loader.h new file mode 100644 index 0000000..1834598 --- /dev/null +++ b/src/core/ktx2_loader.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +// Minimal KTX2 reader for 2D textures with pre-transcoded BCn payloads. +// Supports: faceCount==1, layerCount==1, supercompression==0 (none). +// Extracts vkFormat, base width/height, mip count, and per-level byte ranges. + +struct KTX2LevelInfo { + uint64_t offset{0}; + uint64_t length{0}; + uint32_t width{0}; + uint32_t height{0}; +}; + +struct KTX2Image { + VkFormat format{VK_FORMAT_UNDEFINED}; + uint32_t width{0}; + uint32_t height{0}; + uint32_t mipLevels{0}; + uint32_t faceCount{0}; + uint32_t layerCount{0}; + uint32_t supercompression{0}; + std::vector data; // full file payload to back staging buffer copies + std::vector levels; // level 0..mipLevels-1 +}; + +// Parse from memory. Returns true on success; on failure, 'err' (if provided) describes the issue. +bool parse_ktx2(const uint8_t* bytes, size_t size, KTX2Image& out, std::string* err = nullptr); +