diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt index f54431e773..bf1c1ff30a 100644 --- a/libs/image/CMakeLists.txt +++ b/libs/image/CMakeLists.txt @@ -9,15 +9,17 @@ set(PUBLIC_HDR_DIR include) # ================================================================================================== set(PUBLIC_HDRS include/image/ColorTransform.h - include/image/LinearImage.h - include/image/ImageSampler.h include/image/ImageOps.h + include/image/ImageSampler.h + include/image/KtxBundle.h + include/image/LinearImage.h ) set(SRCS - src/LinearImage.cpp - src/ImageSampler.cpp src/ImageOps.cpp + src/ImageSampler.cpp + src/KtxBundle.cpp + src/LinearImage.cpp ) # ================================================================================================== diff --git a/libs/image/include/image/KtxBundle.h b/libs/image/include/image/KtxBundle.h new file mode 100644 index 0000000000..8d0b4147f0 --- /dev/null +++ b/libs/image/include/image/KtxBundle.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef IMAGE_KTXBUNDLE_H +#define IMAGE_KTXBUNDLE_H + +#include + +namespace image { + +struct KtxInfo { + uint32_t endianness; + uint32_t glType; + uint32_t glTypeSize; + uint32_t glFormat; + uint32_t glInternalFormat; + uint32_t glBaseInternalFormat; + uint32_t pixelWidth; + uint32_t pixelHeight; + uint32_t pixelDepth; +}; + +struct KtxBlobIndex { + uint32_t mipLevel; + uint32_t arrayIndex; + uint32_t cubeFace; +}; + +struct KtxBlobList; + +/** + * KtxBundle is a structured set of opaque data blobs that can be passed straight to the GPU, such + * that a single bundle corresponds to a single texture object. It is well suited for storing + * block-compressed texture data. + * + * One bundle may be comprised of several mipmap levels, cubemap faces, and array elements. The + * number of blobs is immutable, and is determined as follows. + * + * blob_count = mip_count * array_length * (cubemap ? 6 : 1) + * + * Bundles can be quickly serialized to a certain file format (see below link), but this class lives + * in the image lib rather than imageio because it has no dependencies, and does not support CPU + * decoding. + * + * https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/ + * + * WARNING: for now, this class discards the arbitrary key/value data that can be embedded in KTX. + */ +class KtxBundle { +public: + + ~KtxBundle(); + + /** + * Creates a hierarchy of empty texture blobs, to be filled later via setBlob(). + */ + KtxBundle(uint32_t numMipLevels, uint32_t arrayLength, bool isCubemap); + + /** + * Creates a new bundle by deserializing the given data. + * + * Typically, this constructor is used to consume the contents of a KTX file. + */ + KtxBundle(uint8_t const* bytes, uint32_t nbytes); + + /** + * Serializes the bundle into the given target memory. Returns false if there's not enough + * memory. + * + * Typically, this method is used to write out the contents of a KTX file. + */ + bool serialize(uint8_t* destination, uint32_t numBytes) const; + + /** + * Computes the size (in bytes) of the serialized bundle. + */ + uint32_t getSerializedLength() const; + + /** + * Gets or sets information about the texture object, such as format and type. + */ + KtxInfo const& getInfo() const { return mInfo; } + KtxInfo& info() { return mInfo; } + + /** + * Gets the number of miplevels (this is never zero). + */ + uint32_t getNumMipLevels() const { return mNumMipLevels; } + + /** + * Gets the number of array elements (this is never zero). + */ + uint32_t getArrayLength() const { return mArrayLength; } + + /** + * Returns whether or not this is a cubemap. + */ + bool isCubemap() const { return mNumCubeFaces > 1; } + + /** + * Retrieves a weak reference to a given data blob. Returns false if the given blob index is out + * of bounds, or if the blob at the given index is empty. + */ + bool getBlob(KtxBlobIndex index, uint8_t** data, uint32_t* size) const; + + /** + * Copies the given data into the blob at the given index, replacing whatever is already there. + * Returns false if the given blob index is out of bounds. + */ + bool setBlob(KtxBlobIndex index, uint8_t const* data, uint32_t size); + +private: + image::KtxInfo mInfo = {}; + uint32_t mNumMipLevels; + uint32_t mArrayLength; + uint32_t mNumCubeFaces; + KtxBlobList* mBlobs = nullptr; +}; + +} // namespace image + +#endif /* IMAGE_KTXBUNDLE_H */ diff --git a/libs/image/include/image/LinearImage.h b/libs/image/include/image/LinearImage.h index 13e487dfcc..a099221051 100644 --- a/libs/image/include/image/LinearImage.h +++ b/libs/image/include/image/LinearImage.h @@ -17,7 +17,6 @@ #ifndef IMAGE_LINEARIMAGE_H #define IMAGE_LINEARIMAGE_H -#include #include /** diff --git a/libs/image/src/KtxBundle.cpp b/libs/image/src/KtxBundle.cpp new file mode 100644 index 0000000000..30230a88d6 --- /dev/null +++ b/libs/image/src/KtxBundle.cpp @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include + +namespace { + +using Blob = std::vector; + +struct SerializationHeader { + uint8_t magic[12]; + image::KtxInfo info; + uint32_t numberOfArrayElements; + uint32_t numberOfFaces; + uint32_t numberOfMipmapLevels; + uint32_t bytesOfKeyValueData; +}; + +static_assert(sizeof(SerializationHeader) == 16 * 4, "Unexpected header size."); + +// We flatten the three-dimensional blob index using the ordering defined by the KTX spec. +inline size_t flatten(const image::KtxBundle* bundle, image::KtxBlobIndex index) { + const uint32_t nfaces = bundle->isCubemap() ? 6 : 1; + const uint32_t nlayers = bundle->getArrayLength(); + return index.cubeFace + index.arrayIndex * nfaces + index.mipLevel * nfaces * nlayers; +} + +const uint8_t MAGIC[] = {0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a}; + +} + +namespace image { + +// This little wrapper exists so that we can keep STL out of the interface. +struct KtxBlobList { + std::vector blobs; +}; + +KtxBundle::~KtxBundle() { + delete mBlobs; +} + +KtxBundle::KtxBundle(uint32_t numMipLevels, uint32_t arrayLength, bool isCubemap) : + mBlobs(new KtxBlobList) { + mNumMipLevels = numMipLevels; + mArrayLength = arrayLength; + mNumCubeFaces = isCubemap ? 6 : 1; + mBlobs->blobs.resize(numMipLevels * arrayLength * mNumCubeFaces); +} + +KtxBundle::KtxBundle(uint8_t const* bytes, uint32_t nbytes) : mBlobs(new KtxBlobList) { + ASSERT_PRECONDITION(sizeof(SerializationHeader) <= nbytes, "KTX buffer is too small"); + + // First, "parse" the header by casting it to a struct. + SerializationHeader const* header = (SerializationHeader const*) bytes; + ASSERT_PRECONDITION(memcmp(header->magic, MAGIC, 12) == 0, "KTX has unexpected identifier"); + mInfo = header->info; + + // The spec allows 0 or 1 for the number of array layers and mipmap levels, but we replace 0 + // with 1 for simplicity. Technically this is a loss of information because 0 mipmaps means + // "please generate the mips" and an array size of 1 means "make this an array texture, but + // with only one element". For now, ignoring this distinction seems fine. + mNumMipLevels = header->numberOfMipmapLevels ? header->numberOfMipmapLevels : 1; + mArrayLength = header->numberOfArrayElements ? header->numberOfArrayElements : 1; + mNumCubeFaces = header->numberOfFaces ? header->numberOfFaces : 1; + mBlobs->blobs.resize(mNumMipLevels * mArrayLength * mNumCubeFaces); + + // For now, we discard the key-value metadata. Note that this may be useful for storing + // spherical harmonics coefficients. + uint8_t const* pdata = bytes + sizeof(SerializationHeader); + uint8_t const* end = pdata + header->bytesOfKeyValueData; + while (pdata < end) { + const uint32_t keyAndValueByteSize = *((uint32_t const*) pdata); + pdata += sizeof(keyAndValueByteSize); + // ...this is a good spot for stashing the keyAndValue block... + pdata += keyAndValueByteSize; + const uint32_t paddingSize = 3 - ((keyAndValueByteSize + 3) % 4); + pdata += paddingSize; + } + + // There is no compressed format that has a block size that is not a multiple of 4, so these + // two padding constants can be safely hardcoded to 0. They are here for spec consistency. + const uint32_t cubePadding = 0; + const uint32_t mipPadding = 0; + + // One aspect of the KTX spec is that the semantics differ for non-array cubemaps. + const bool isNonArrayCube = mNumCubeFaces > 1 && mArrayLength == 1; + const uint32_t facesPerMip = mArrayLength * mNumCubeFaces; + + // Extract blobs from the serialized byte stream. + for (uint32_t mipmap = 0; mipmap < mNumMipLevels; ++mipmap) { + const uint32_t imageSize = *((uint32_t const*) pdata); + const uint32_t faceSize = isNonArrayCube ? imageSize : (imageSize / facesPerMip); + pdata += sizeof(imageSize); + for (uint32_t layer = 0; layer < mArrayLength; ++layer) { + for (uint32_t face = 0; face < mNumCubeFaces; ++face) { + setBlob({mipmap, layer, face}, pdata, faceSize); + pdata += faceSize; + pdata += cubePadding; + } + } + pdata += mipPadding; + } +} + +bool KtxBundle::serialize(uint8_t* destination, uint32_t numBytes) const { + uint32_t requiredLength = getSerializedLength(); + if (numBytes < requiredLength) { + return false; + } + + // Fill in the header with the magic identifier, format info, and dimensions. + SerializationHeader header = {}; + memcpy(header.magic, MAGIC, sizeof(MAGIC)); + header.info = mInfo; + header.numberOfMipmapLevels = mNumMipLevels; + header.numberOfArrayElements = mArrayLength; + header.numberOfFaces = mNumCubeFaces; + + // For simplicity, KtxBundle does not allow non-zero array length, but to be conformant we + // should set this field to zero for non-array textures. + if (mArrayLength == 1) { + header.numberOfArrayElements = 0; + } + + // Copy the header into the destination memory. + memcpy(destination, &header, sizeof(header)); + uint8_t* pdata = destination + sizeof(SerializationHeader); + + // One aspect of the KTX spec is that the semantics differ for non-array cubemaps. + const bool isNonArrayCube = mNumCubeFaces > 1 && mArrayLength == 1; + const uint32_t facesPerMip = mArrayLength * mNumCubeFaces; + + // Extract blobs from the serialized byte stream. + for (uint32_t mipmap = 0; mipmap < mNumMipLevels; ++mipmap) { + + // Every blob in a given miplevel has the same size, and each miplevel has at least one + // blob. Therefore we can safely determine each of the so-called "imageSize" fields in KTX + // by simply looking at the first blob in the LOD. + uint32_t faceSize; + uint8_t* blobData; + getBlob({mipmap, 0, 0}, &blobData, &faceSize); + uint32_t imageSize = isNonArrayCube ? faceSize : (faceSize * facesPerMip); + *((uint32_t*) pdata) = imageSize; + pdata += sizeof(imageSize); + + // Next, copy out the actual blobs. + for (uint32_t layer = 0; layer < mArrayLength; ++layer) { + for (uint32_t face = 0; face < mNumCubeFaces; ++face) { + if (!getBlob({mipmap, layer, face}, &blobData, &faceSize)) { + return false; + } + memcpy(pdata, blobData, faceSize); + pdata += faceSize; + } + } + } + return true; +} + +uint32_t KtxBundle::getSerializedLength() const { + uint32_t total = sizeof(SerializationHeader); + for (uint32_t mipmap = 0; mipmap < mNumMipLevels; ++mipmap) { + total += sizeof(uint32_t); + size_t blobSize = 0; + for (uint32_t layer = 0; layer < mArrayLength; ++layer) { + for (uint32_t face = 0; face < mNumCubeFaces; ++face) { + auto& blob = mBlobs->blobs[flatten(this, {mipmap, layer, face})]; + if (blobSize == 0) { + blobSize = blob.size(); + } + ASSERT_PRECONDITION(blobSize == blob.size(), "Inconsistent blob sizes within LOD"); + total += blobSize; + } + } + } + return total; +} + +bool KtxBundle::getBlob(KtxBlobIndex index, uint8_t** data, uint32_t* size) const { + if (index.mipLevel >= mNumMipLevels || index.arrayIndex >= mArrayLength || + index.cubeFace >= mNumCubeFaces) { + return false; + } + auto& blob = mBlobs->blobs[flatten(this, index)]; + if (blob.empty()) { + return false; + } + *data = blob.data(); + *size = blob.size(); + return true; +} + +bool KtxBundle::setBlob(KtxBlobIndex index, uint8_t const* data, uint32_t size) { + if (index.mipLevel >= mNumMipLevels || index.arrayIndex >= mArrayLength || + index.cubeFace >= mNumCubeFaces) { + return false; + } + auto& blob = mBlobs->blobs[flatten(this, index)]; + blob.resize(size); + memcpy(blob.data(), data, size); + return true; +} + +} // namespace image diff --git a/libs/image/tests/reference/conftestimage_R11_EAC.ktx b/libs/image/tests/reference/conftestimage_R11_EAC.ktx new file mode 100644 index 0000000000..caea99f8df Binary files /dev/null and b/libs/image/tests/reference/conftestimage_R11_EAC.ktx differ diff --git a/libs/image/tests/test_image.cpp b/libs/image/tests/test_image.cpp index 6b8279bc17..3ea2b66cad 100644 --- a/libs/image/tests/test_image.cpp +++ b/libs/image/tests/test_image.cpp @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -34,10 +35,12 @@ #include #include #include +#include using std::istringstream; using std::string; using std::swap; +using std::vector; using math::float3; using math::float4; @@ -287,7 +290,7 @@ TEST_F(ImageTest, Mipmaps) { // NOLINT "44444 41014 40704 41014 44444 44444 41014 40704 41014 44444"); uint32_t count = getMipmapCount(src); ASSERT_EQ(count, 3); - std::vector mips(count); + vector mips(count); generateMipmaps(src, filter, mips.data(), count); updateOrCompare(src, "mip0_5x10.png"); for (uint32_t index = 0; index < count; ++index) { @@ -307,6 +310,58 @@ TEST_F(ImageTest, Mipmaps) { // NOLINT } } +TEST_F(ImageTest, Ktx) { // NOLINT + uint8_t foo[] = {1, 2, 3}; + uint8_t* data; + uint32_t size; + KtxBundle nascent(2, 1, true); + ASSERT_EQ(nascent.getNumMipLevels(), 2); + ASSERT_EQ(nascent.getArrayLength(), 1); + ASSERT_TRUE(nascent.isCubemap()); + ASSERT_FALSE(nascent.getBlob({0, 0, 0}, &data, &size)); + ASSERT_TRUE(nascent.setBlob({0, 0, 0}, foo, sizeof(foo))); + ASSERT_TRUE(nascent.getBlob({0, 0, 0}, &data, &size)); + ASSERT_EQ(size, sizeof(foo)); + + const uint32_t KTX_HEADER_SIZE = 16 * 4; + + auto getFileSize = [](const char* filename) { + std::ifstream in(filename, std::ifstream::ate | std::ifstream::binary); + return in.tellg(); + }; + + if (g_comparisonMode == ComparisonMode::COMPARE) { + const auto path = g_comparisonPath + "conftestimage_R11_EAC.ktx"; + const auto fileSize = getFileSize(path.c_str()); + ASSERT_GT(fileSize, 0); + vector buffer(fileSize); + std::ifstream in(path, std::ifstream::in); + ASSERT_TRUE(in.read((char*) buffer.data(), fileSize)); + KtxBundle deserialized(buffer.data(), buffer.size()); + + ASSERT_EQ(deserialized.getNumMipLevels(), 1); + ASSERT_EQ(deserialized.getArrayLength(), 1); + ASSERT_EQ(deserialized.isCubemap(), false); + ASSERT_EQ(deserialized.getInfo().pixelWidth, 64); + ASSERT_EQ(deserialized.getInfo().pixelHeight, 32); + ASSERT_EQ(deserialized.getInfo().pixelDepth, 0); + + data = nullptr; + size = 0; + ASSERT_TRUE(deserialized.getBlob({0, 0, 0}, &data, &size)); + ASSERT_EQ(size, 1024); + ASSERT_NE(data, nullptr); + + uint32_t serializedSize = deserialized.getSerializedLength(); + ASSERT_EQ(serializedSize, KTX_HEADER_SIZE + sizeof(uint32_t) + 1024); + ASSERT_EQ(serializedSize, fileSize); + + vector reserialized(serializedSize); + ASSERT_TRUE(deserialized.serialize(reserialized.data(), serializedSize)); + ASSERT_EQ(reserialized, buffer); + } +} + static void printUsage(const char* name) { string exec_name(utils::Path(name).getName()); string usage(