// Copyright 2021 Paolo Jovon, All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 #include #include #include #include #include #include #include #include #include #include "gl_format.h" #include "ktx.h" #include "gtest/gtest.h" namespace { constexpr const char SAMPLE_KTX1[] = "pattern_02_bc2.ktx"; constexpr const char SAMPLE_KTX2[] = "pattern_02_bc2.ktx2"; std::string testImagesPath; std::unique_ptr testImageFilebuf(std::string name) { std::string imagePath{testImagesPath}; imagePath += '/'; imagePath += name; auto filebuf = std::make_unique(); filebuf->open(imagePath, std::ios::in | std::ios::binary); if (filebuf->is_open()) { return filebuf; } return nullptr; } /// A ktxStream that wraps a C++ std::streambuf. class StreambufStream { // Doubt this will ever get triggered static_assert(sizeof(char) == sizeof(uint8_t), "Chars are != 1 byte in this platform"); public: StreambufStream(std::unique_ptr &&streambuf, std::ios::openmode seek_mode = std::ios::in | std::ios::out) : _streambuf{std::move(streambuf)} , _seek_mode{seek_mode} , _stream{std::make_unique()} , _destructed{false} { _stream->type = eStreamTypeCustom; _stream->closeOnDestruct = false; auto& custom_ptr = _stream->data.custom_ptr; custom_ptr.address = this; custom_ptr.allocatorAddress = nullptr; // N/A custom_ptr.size = 0; // N/A _stream->read = read; _stream->skip = skip; _stream->write = write; _stream->getpos = getpos; _stream->setpos = setpos; _stream->getsize = getsize; _stream->destruct = destruct; } StreambufStream(const StreambufStream&) = delete; StreambufStream &operator=(const StreambufStream&) = delete; StreambufStream(StreambufStream&&) = delete; StreambufStream &operator=(StreambufStream&&) = delete; virtual ~StreambufStream() { EXPECT_TRUE(_destructed) << "ktxStream should have been destructed"; } inline ktxStream* stream() const { return _stream.get(); } inline std::streambuf* streambuf() const { return _streambuf.get(); } inline std::ios::openmode seek_mode() const { return _seek_mode; } inline void seek_mode(std::ios::openmode newmode) { _seek_mode = newmode; } inline bool destructed() const { return _destructed; } protected: // C++ streambuf overrides // ktxStream vtable implementations inline static StreambufStream* parent(ktxStream *str) { return reinterpret_cast(str->data.custom_ptr.address); } static KTX_error_code read(ktxStream* str, void* dst, ktx_size_t count) { auto self = parent(str); if (count == 0) { return KTX_SUCCESS; } std::cerr << "\t read: " << count << 'B' << std::endl; const auto stdcount = std::streamsize(count); const std::streamsize nread = self->_streambuf->sgetn(reinterpret_cast(dst), stdcount); return (nread == stdcount) ? KTX_SUCCESS : KTX_FILE_UNEXPECTED_EOF; } static KTX_error_code skip(ktxStream* str, ktx_size_t count) { auto self = parent(str); if (count == 0) { return KTX_SUCCESS; } std::cerr << "\t skip: " << count << 'B' << std::endl; const std::streampos curpos = self->_streambuf->pubseekoff(0, std::ios::cur, self->_seek_mode); const std::streampos newpos = self->_streambuf->pubseekoff(std::streamoff(count), std::ios::cur, self->_seek_mode); return (curpos > newpos) ? KTX_SUCCESS : KTX_FILE_SEEK_ERROR; } static KTX_error_code write(ktxStream* str, const void* src, ktx_size_t size, ktx_size_t count) { auto self = parent(str); if (size == 0 || count == 0) { return KTX_SUCCESS; } std::cerr << "\t write: " << count << "*" << size << "B" << std::endl; const auto ntotal = std::streamsize(size * count); const std::streamsize nput = self->_streambuf->sputn(reinterpret_cast(src), ntotal); return (nput == ntotal) ? KTX_SUCCESS : KTX_FILE_WRITE_ERROR; } static KTX_error_code getpos(ktxStream* str, ktx_off_t *offset) { auto self = parent(str); *offset = ktx_off_t(self->_streambuf->pubseekoff(0, std::ios::cur, self->_seek_mode)); std::cerr << "\tgetpos: " << *offset << std::endl; return KTX_SUCCESS; } static KTX_error_code setpos(ktxStream* str, ktx_off_t offset) { auto self = parent(str); const auto newpos = std::streamoff(offset); const std::streampos setpos = self->_streambuf->pubseekoff(newpos, std::ios::beg, self->_seek_mode); std::cerr << "\tsetpos: " << offset << std::endl; return (setpos == newpos) ? KTX_SUCCESS : KTX_FILE_SEEK_ERROR; } static KTX_error_code getsize(ktxStream* str, ktx_size_t* size) { auto self = parent(str); const std::streampos oldpos = self->_streambuf->pubseekoff(0, std::ios::cur, self->_seek_mode); *size = ktx_size_t(self->_streambuf->pubseekoff(0, std::ios::end)); const std::streampos newpos = self->_streambuf->pubseekoff(oldpos, std::ios::beg, self->_seek_mode); std::cerr << "\t size: " << *size << 'B' << std::endl; return (oldpos == newpos) ? KTX_SUCCESS : KTX_FILE_SEEK_ERROR; } static void destruct(ktxStream* str) { auto self = parent(str); self->_destructed = true; } std::unique_ptr _streambuf; std::ios::openmode _seek_mode; std::unique_ptr _stream; bool _destructed; }; class ktxStreamTest : public ::testing::Test { protected: void SetUp() override { _ktx1Streambuf = testImageFilebuf(SAMPLE_KTX1); ASSERT_TRUE(_ktx1Streambuf) << "Could not load sample KTX1"; _ktx2Streambuf = testImageFilebuf(SAMPLE_KTX2); ASSERT_TRUE(_ktx2Streambuf) << "Could not load sample KTX2"; } void TearDown() override { _ktx1Streambuf.reset(); _ktx2Streambuf.reset(); } std::unique_ptr _ktx1Streambuf; std::unique_ptr _ktx2Streambuf; }; /// A RAIIfied ktxTexture. template class KtxTexture final { public: KtxTexture(std::nullptr_t null = nullptr) : _handle{nullptr} { (void)null; } KtxTexture(T* handle) : _handle{handle} { } KtxTexture(const KtxTexture&) = delete; KtxTexture &operator=(const KtxTexture&) = delete; KtxTexture(KtxTexture&& toMove) : _handle{toMove._handle} { toMove._handle = nullptr; } KtxTexture &operator=(KtxTexture&& toMove) { _handle = toMove._handle; toMove._handle = nullptr; return *this; } ~KtxTexture() { if (_handle) { ktxTexture_Destroy(handle()); _handle = nullptr; } } template inline U* handle() const { return reinterpret_cast(_handle); } template inline U** pHandle() { return reinterpret_cast(&_handle); } inline operator T*() const { return _handle; } private: T* _handle; }; /// Expects two textures to be equal in content (but not necessarily be the same texture). bool expectSameTextureContent(const ktxTexture* tex1, const ktxTexture* tex2) { bool ok = true; #define EXPECT_EQ_OK(val1, val2) \ ok = ok && (val1) == (val2); \ EXPECT_EQ(val1, val2) EXPECT_EQ_OK(tex1->classId, tex2->classId) << "Mismatched texture type (KTX1 or KTX2)"; EXPECT_EQ_OK(tex1->isArray, tex2->isArray) << "Both textures should [not] be array textures"; EXPECT_EQ_OK(tex1->isCubemap, tex2->isCubemap) << "Both textures should [not] be cubemap [arrays]"; EXPECT_EQ_OK(tex1->isCompressed, tex2->isCompressed) << "Both textures should [not] be compressed"; EXPECT_EQ_OK(tex1->baseWidth, tex2->baseWidth) << "Mismatched base width"; EXPECT_EQ_OK(tex1->baseHeight, tex2->baseHeight) << "Mismatched base height"; EXPECT_EQ_OK(tex1->baseDepth, tex2->baseDepth) << "Mismatched base depth"; EXPECT_EQ_OK(tex1->numDimensions, tex2->numDimensions) << "Mismatched # of texture dimensions"; EXPECT_EQ_OK(tex1->numLevels, tex2->numLevels) << "Mismatched # of texture levels"; EXPECT_EQ_OK(tex1->numLayers, tex2->numLayers) << "Mismatched # of texture layers"; EXPECT_EQ_OK(tex1->numFaces, tex2->numFaces) << "Mismatched # of texture faces"; EXPECT_EQ_OK(tex1->orientation.x, tex2->orientation.x) << "Mismatched X orientation"; EXPECT_EQ_OK(tex1->orientation.y, tex2->orientation.y) << "Mismatched Y orientation"; EXPECT_EQ_OK(tex1->orientation.z, tex2->orientation.z) << "Mismatched Z orientation"; EXPECT_EQ_OK(tex1->kvDataLen, tex2->kvDataLen) << "Mismatched K/V data length"; auto* e1 = ktxHashList_Next(tex1->kvDataHead); auto* e2 = ktxHashList_Next(tex2->kvDataHead); for(size_t i = 0; e1 && e2; e1 = ktxHashList_Next(e1), e2 = ktxHashList_Next(e2), i++) { unsigned int len1 = 0, len2 = 0; { char *key1 = nullptr, *key2 = nullptr; (void)ktxHashListEntry_GetKey(e1, &len1, &key1); (void)ktxHashListEntry_GetKey(e2, &len2, &key2); EXPECT_EQ_OK(strncmp(key1, key2, std::min(len1, len2)), 0) << i << "th key mismatch"; } { void *val1 = nullptr, *val2 = nullptr; (void)ktxHashListEntry_GetValue(e1, &len1, &val1); (void)ktxHashListEntry_GetValue(e2, &len2, &val2); EXPECT_EQ_OK(memcmp(val1, val2, std::min(len1, len2)), 0) << i << "th value mismatch"; } } EXPECT_EQ_OK(tex1->dataSize, tex2->dataSize) << "Mismatched image data size"; EXPECT_EQ_OK(memcmp(tex1->pData, tex2->pData, std::min(tex1->dataSize, tex2->dataSize)), 0) << "Mismatched image data"; #undef EXPECT_EQ_OK return ok; } // --- Tests --- TEST_F(ktxStreamTest, CanCreateKtx1FromCppStream) { StreambufStream ktx1Stream{std::move(_ktx1Streambuf), std::ios::in}; KtxTexture texture1; KTX_error_code err = ktxTexture1_CreateFromStream(ktx1Stream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, texture1.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to create KTX1 from C++ stream: " << ktxErrorString(err); ASSERT_NE(texture1, nullptr) << "Newly-created KTX1 is null"; EXPECT_TRUE(ktx1Stream.destructed()) << "ktxStream should have been destructed (LOAD_IMAGE_DATA_BIT set)"; } TEST_F(ktxStreamTest, CanCreateKtx2FromCppStream) { StreambufStream ktx2Stream{std::move(_ktx2Streambuf), std::ios::in}; KtxTexture texture2; KTX_error_code err = ktxTexture2_CreateFromStream(ktx2Stream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, texture2.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to create KTX2 from C++ stream: " << ktxErrorString(err); ASSERT_NE(texture2, nullptr) << "Newly-created KTX2 is null"; EXPECT_TRUE(ktx2Stream.destructed()) << "ktxStream should have been destructed (LOAD_IMAGE_DATA_BIT set)"; } TEST_F(ktxStreamTest, CanCreateAutoKtxFromCppStream) { StreambufStream ktxStream{std::move(_ktx2Streambuf), std::ios::in}; // Or could use the KTx1, no difference KtxTexture texture; KTX_error_code err = ktxTexture_CreateFromStream(ktxStream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, texture.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to create auto-detected KTX from C++ stream: " << ktxErrorString(err); ASSERT_NE(texture, nullptr) << "Newly-created auto-detected KTX is null"; EXPECT_TRUE(ktxStream.destructed()) << "ktxStream should have been destructed (LOAD_IMAGE_DATA_BIT set)"; } TEST_F(ktxStreamTest, CanWriteKtx1AsKtx2ToCppStream) { KTX_error_code err{KTX_INVALID_VALUE}; auto dstStreambuf = std::make_unique(); StreambufStream dstStream{std::move(dstStreambuf)}; KtxTexture srcTexture1{nullptr}; KtxTexture dstTexture2{nullptr}; { std::cerr << "Loading KTX1 from file" << std::endl; StreambufStream srcStream{std::move(_ktx1Streambuf), std::ios::in}; err = ktxTexture1_CreateFromStream(srcStream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, srcTexture1.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to load source KTX1 from C++ stream: " << ktxErrorString(err); ASSERT_NE(srcTexture1, nullptr) << "Source KTX1 is null"; EXPECT_TRUE(srcStream.destructed()) << "ktxStream should have been destructed (LOAD_IMAGE_DATA_BIT set)"; } { std::cerr << "Converting KTX1 -> KTX2" << std::endl; // We're about to write to `dstStream` dstStream.seek_mode(std::ios::out); err = ktxTexture1_WriteKTX2ToStream(srcTexture1, dstStream.stream()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to convert KTX1 -> KTX2 to C++ stream: " << ktxErrorString(err); } { std::cerr << "Loading the converted KTX2" << std::endl; // Rewind dstStream and set it up for reading dstStream.streambuf()->pubseekpos(0, std::ios::in); dstStream.seek_mode(std::ios::in); err = ktxTexture2_CreateFromStream(dstStream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, dstTexture2.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to load converted KTX2 from C++ stream: " << ktxErrorString(err); ASSERT_NE(dstTexture2, nullptr) << "Destination KTX2 is null"; } } TEST_F(ktxStreamTest, CanWriteKtx2ToCppStream) { KTX_error_code err{KTX_INVALID_VALUE}; auto dstStreambuf = std::make_unique(); StreambufStream dstStream{std::move(dstStreambuf)}; KtxTexture srcTexture2; KtxTexture dstTexture2; { std::cerr << "Loading KTX2 from file" << std::endl; StreambufStream srcStream{std::move(_ktx2Streambuf), std::ios::in}; err = ktxTexture2_CreateFromStream(srcStream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, srcTexture2.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to load source KTX2 from C++ stream: " << ktxErrorString(err); ASSERT_NE(srcTexture2, nullptr) << "Source KTX2 is null"; EXPECT_TRUE(srcStream.destructed()) << "ktxStream should have been destructed (LOAD_IMAGE_DATA_BIT set)"; } { std::cerr << "Writing KTX2 -> copied KTX2" << std::endl; // We're about to write to `dstStream` dstStream.seek_mode(std::ios::out); err = ktxTexture_WriteToStream(srcTexture2.handle(), dstStream.stream()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to convert KTX1 -> KTX2 to C++ stream: " << ktxErrorString(err); } { std::cerr << "Loading the converted KTX2" << std::endl; // Rewind dstStream and set it up for reading dstStream.streambuf()->pubseekpos(0, std::ios::in); dstStream.seek_mode(std::ios::in); err = ktxTexture2_CreateFromStream(dstStream.stream(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, dstTexture2.pHandle()); EXPECT_EQ(err, KTX_SUCCESS) << "Failed to load converted KTX2 from C++ stream: " << ktxErrorString(err); ASSERT_NE(dstTexture2, nullptr) << "Destination KTX2 is null"; } // Should be a clone of the same texture expectSameTextureContent(srcTexture2.handle(), dstTexture2.handle()); } } // namespace int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); if (!::testing::FLAGS_gtest_list_tests) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " \n"; return -1; } testImagesPath = argv[1]; struct stat info; if (stat(testImagesPath.data(), &info) != 0) { std::cerr << "Cannot access " << testImagesPath << '\n'; return -2; } else if (!(info.st_mode & S_IFDIR)) { std::cerr << testImagesPath << "is not a valid directory\n"; return -3; } } return RUN_ALL_TESTS(); }