Introduce KtxBundle to libimage and add unit test.
KtxBundle is basically an in-memory representation of a KTX file. Our primary motivation for introducing this class is to have a structured container for block-compressed textures. Note that this class is defined in libimage rather than libimageio. There are several reasons for this: 1. The texel data in a KtxBundle is not meant to be decoded by the CPU. 2. It has no dependendencies on any libraries (stb_image tinyexr etc). 3. We wish to use it with WebGL, which needs to avoid using imageio.
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
# ==================================================================================================
|
||||
|
||||
135
libs/image/include/image/KtxBundle.h
Normal file
135
libs/image/include/image/KtxBundle.h
Normal file
@@ -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 <cstdint>
|
||||
|
||||
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 */
|
||||
@@ -17,7 +17,6 @@
|
||||
#ifndef IMAGE_LINEARIMAGE_H
|
||||
#define IMAGE_LINEARIMAGE_H
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
|
||||
222
libs/image/src/KtxBundle.cpp
Normal file
222
libs/image/src/KtxBundle.cpp
Normal file
@@ -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 <image/KtxBundle.h>
|
||||
|
||||
#include <utils/Panic.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using Blob = std::vector<uint8_t>;
|
||||
|
||||
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<Blob> 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
|
||||
BIN
libs/image/tests/reference/conftestimage_R11_EAC.ktx
Normal file
BIN
libs/image/tests/reference/conftestimage_R11_EAC.ktx
Normal file
Binary file not shown.
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
#include <image/ColorTransform.h>
|
||||
#include <image/KtxBundle.h>
|
||||
#include <image/ImageOps.h>
|
||||
#include <image/ImageSampler.h>
|
||||
#include <image/LinearImage.h>
|
||||
@@ -34,10 +35,12 @@
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
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<LinearImage> mips(count);
|
||||
vector<LinearImage> 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<uint8_t> 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<uint8_t> 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(
|
||||
|
||||
Reference in New Issue
Block a user