| #include "rive_testing.hpp" |
| #include "rive/decoders/decode_ktx2.hpp" |
| |
| #include <cstdint> |
| #include <cstring> |
| #include <vector> |
| |
| // Helpers for building a synthetic KTX2 byte stream so the tests can exercise |
| // each rejection path of the parser without committing a binary fixture. |
| // |
| // Layout: |
| // [12] identifier «KTX 20»\r\n\x1A\n |
| // [68] header |
| // [24*levelCount] level index |
| // [DFD] data format descriptor (variable, often empty here) |
| // ...mip data |
| namespace |
| { |
| constexpr uint8_t Ktx2Identifier[12] = { |
| 0xAB, |
| 0x4B, |
| 0x54, |
| 0x58, |
| 0x20, |
| 0x32, |
| 0x30, |
| 0xBB, |
| 0x0D, |
| 0x0A, |
| 0x1A, |
| 0x0A, |
| }; |
| constexpr uint32_t VK_FORMAT_BC7_SRGB_BLOCK = 146; |
| |
| template <typename T> void appendLE(std::vector<uint8_t>& buf, T value) |
| { |
| const uint8_t* p = reinterpret_cast<const uint8_t*>(&value); |
| buf.insert(buf.end(), p, p + sizeof(T)); |
| } |
| |
| std::vector<uint8_t> buildSkeletonKtx2(uint32_t vkFormat, |
| uint32_t pixelWidth, |
| uint32_t pixelHeight, |
| uint32_t levelCount, |
| uint32_t supercompressionScheme, |
| uint32_t faceCount, |
| uint32_t layerCount) |
| { |
| std::vector<uint8_t> buf; |
| buf.insert(buf.end(), |
| Ktx2Identifier, |
| Ktx2Identifier + sizeof(Ktx2Identifier)); |
| |
| appendLE<uint32_t>(buf, vkFormat); |
| appendLE<uint32_t>(buf, 1); // typeSize |
| appendLE<uint32_t>(buf, pixelWidth); |
| appendLE<uint32_t>(buf, pixelHeight); |
| appendLE<uint32_t>(buf, 0); // pixelDepth |
| appendLE<uint32_t>(buf, layerCount); |
| appendLE<uint32_t>(buf, faceCount); |
| appendLE<uint32_t>(buf, levelCount); |
| appendLE<uint32_t>(buf, supercompressionScheme); |
| appendLE<uint32_t>(buf, 0); // dfdByteOffset |
| appendLE<uint32_t>(buf, 0); // dfdByteLength |
| appendLE<uint32_t>(buf, 0); // kvdByteOffset |
| appendLE<uint32_t>(buf, 0); // kvdByteLength |
| appendLE<uint64_t>(buf, 0); // sgdByteOffset |
| appendLE<uint64_t>(buf, 0); // sgdByteLength |
| return buf; |
| } |
| } // namespace |
| |
| TEST_CASE("ktx2 rejects buffer smaller than identifier+header", |
| "[ktx2-decoder]") |
| { |
| std::vector<uint8_t> buf(40, 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 rejects bad magic", "[ktx2-decoder]") |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 4, 4, 1, 0, 1, 0); |
| buf[0] = 'X'; |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 rejects unsupported vkFormat", "[ktx2-decoder]") |
| { |
| // VK_FORMAT_R8G8B8A8_UNORM = 37 — not BC7. |
| auto buf = buildSkeletonKtx2(/*vkFormat*/ 37, 4, 4, 1, 0, 1, 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 rejects supercompressed payload (not yet supported)", |
| "[ktx2-decoder]") |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, |
| 4, |
| 4, |
| 1, |
| /*supercompressionScheme*/ 2 /* zstd */, |
| 1, |
| 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 rejects cubemaps and array layers", "[ktx2-decoder]") |
| { |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, |
| 4, |
| 4, |
| 1, |
| 0, |
| /*faceCount*/ 6, |
| 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, |
| 4, |
| 4, |
| 1, |
| 0, |
| 1, |
| /*layerCount*/ 4); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| } |
| |
| TEST_CASE("ktx2 rejects out-of-range dimensions", "[ktx2-decoder]") |
| { |
| { |
| auto buf = |
| buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 0, 4, 1, 0, 1, 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| { |
| auto buf = |
| buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 4, 999999, 1, 0, 1, 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| } |
| |
| TEST_CASE("ktx2 rejects truncated level index", "[ktx2-decoder]") |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, |
| 4, |
| 4, |
| /*levelCount*/ 1, |
| 0, |
| 1, |
| 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 rejects level pointer outside buffer", "[ktx2-decoder]") |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 4, 4, 1, 0, 1, 0); |
| appendLE<uint64_t>(buf, /*byteOffset*/ 1ull << 32); |
| appendLE<uint64_t>(buf, /*byteLength*/ 16); |
| appendLE<uint64_t>(buf, /*uncompressedByteLength*/ 16); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 rejects byteLength inconsistent with logical block grid", |
| "[ktx2-decoder]") |
| { |
| // 4x4 image = 1 BC7 block = 16 bytes. Claiming 32 bytes mismatches. |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 4, 4, 1, 0, 1, 0); |
| const uint64_t levelOffset = buf.size() + 24; |
| appendLE<uint64_t>(buf, levelOffset); |
| appendLE<uint64_t>(buf, /*byteLength*/ 32); |
| appendLE<uint64_t>(buf, 32); |
| buf.resize(buf.size() + 32, 0); |
| rive::Ktx2DecodeResult out; |
| REQUIRE_FALSE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| } |
| |
| TEST_CASE("ktx2 happy path: single 4x4 BC7 mip 0", "[ktx2-decoder]") |
| { |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 4, 4, 1, 0, 1, 0); |
| const uint64_t levelOffset = buf.size() + 24; |
| appendLE<uint64_t>(buf, levelOffset); |
| appendLE<uint64_t>(buf, /*byteLength*/ 16); |
| appendLE<uint64_t>(buf, /*uncompressedByteLength*/ 16); |
| // 16 bytes of synthetic block payload — parser doesn't validate the |
| // BC7 bitstream, just copies the bytes through. |
| const uint8_t expected[16] = { |
| 0xDE, |
| 0xAD, |
| 0xBE, |
| 0xEF, |
| 0x01, |
| 0x02, |
| 0x03, |
| 0x04, |
| 0x05, |
| 0x06, |
| 0x07, |
| 0x08, |
| 0xCA, |
| 0xFE, |
| 0xBA, |
| 0xBE, |
| }; |
| buf.insert(buf.end(), expected, expected + 16); |
| |
| rive::Ktx2DecodeResult out; |
| REQUIRE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| REQUIRE(out.format == rive::GPUTextureFormat::bc7); |
| REQUIRE(out.pixelWidth == 4); |
| REQUIRE(out.pixelHeight == 4); |
| REQUIRE(out.levelCount == 1); |
| REQUIRE(out.blocks.size() == 16); |
| REQUIRE(std::memcmp(out.blocks.data(), expected, 16) == 0); |
| } |
| |
| TEST_CASE("ktx2 happy path: 8x8 with two mip levels concatenated", |
| "[ktx2-decoder]") |
| { |
| // Mip 0 = 8x8 = 4 blocks (64 bytes). Mip 1 = 4x4 = 1 block (16 bytes). |
| // Level index lists level 0 first; on disk levels should sit smallest- |
| // first but the parser only reads each level by its own offset, so |
| // ordering within the buffer doesn't matter here. |
| auto buf = buildSkeletonKtx2(VK_FORMAT_BC7_SRGB_BLOCK, 8, 8, 2, 0, 1, 0); |
| |
| const uint64_t headerEnd = buf.size(); |
| const uint64_t levelIndexBytes = 24 * 2; |
| const uint64_t mip0Offset = headerEnd + levelIndexBytes; |
| const uint64_t mip0Bytes = 64; |
| const uint64_t mip1Offset = mip0Offset + mip0Bytes; |
| const uint64_t mip1Bytes = 16; |
| |
| // Level index entry 0 (mip 0, 8x8). |
| appendLE<uint64_t>(buf, mip0Offset); |
| appendLE<uint64_t>(buf, mip0Bytes); |
| appendLE<uint64_t>(buf, mip0Bytes); |
| // Level index entry 1 (mip 1, 4x4). |
| appendLE<uint64_t>(buf, mip1Offset); |
| appendLE<uint64_t>(buf, mip1Bytes); |
| appendLE<uint64_t>(buf, mip1Bytes); |
| |
| // Block payloads. Distinct fill bytes so the test can verify ordering. |
| buf.resize(buf.size() + mip0Bytes, 0xAA); |
| buf.resize(buf.size() + mip1Bytes, 0xBB); |
| |
| rive::Ktx2DecodeResult out; |
| REQUIRE(rive::DecodeKtx2(buf.data(), buf.size(), out)); |
| REQUIRE(out.pixelWidth == 8); |
| REQUIRE(out.pixelHeight == 8); |
| REQUIRE(out.levelCount == 2); |
| REQUIRE(out.blocks.size() == mip0Bytes + mip1Bytes); |
| // Output buffer is concatenated level 0 (largest) first, then level 1. |
| REQUIRE(out.blocks[0] == 0xAA); |
| REQUIRE(out.blocks[mip0Bytes - 1] == 0xAA); |
| REQUIRE(out.blocks[mip0Bytes] == 0xBB); |
| REQUIRE(out.blocks[mip0Bytes + mip1Bytes - 1] == 0xBB); |
| } |