Add ktx
This commit is contained in:
@@ -0,0 +1,990 @@
|
||||
/* -*- tab-width: 4; -*- */
|
||||
/* vi: set sw=2 ts=4 expandtab: */
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @file
|
||||
* @~English
|
||||
*
|
||||
* @brief Functions for creating KTX-format files from a set of images.
|
||||
*
|
||||
* @author Mark Callow, HI Corporation
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright 2018-2020 Mark Callow.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#ifdef _WIN32
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#if defined(__GNUC__)
|
||||
#include <strings.h> // For strncasecmp on GNU/Linux
|
||||
#endif
|
||||
#include <zstd.h>
|
||||
#include <zstd_errors.h>
|
||||
#include <KHR/khr_df.h>
|
||||
|
||||
#include "ktx.h"
|
||||
#include "ktxint.h"
|
||||
#include "filestream.h"
|
||||
#include "memstream.h"
|
||||
#include "texture2.h"
|
||||
|
||||
#include "dfdutils/dfd.h"
|
||||
#include "vkformat_enum.h"
|
||||
#include "vk_format.h"
|
||||
|
||||
#include "version.h"
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#define strncasecmp _strnicmp
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @defgroup writer Writer
|
||||
* @brief Write KTX-formatted data.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#if defined(_WIN32) || defined(linux) || defined(__linux) || defined(__linux__) || defined(__EMSCRIPTEN__)
|
||||
/** @internal
|
||||
* @~English
|
||||
* @brief strnstr for Windows, Linux and Emscripten.
|
||||
*
|
||||
* strnstr is available in <apple>OS and BSD distributions. To use in Linux
|
||||
* requires linking an additional library, libbsd. It is simpler to use ours.
|
||||
*
|
||||
* @param[in] haystack pointer to string to search.
|
||||
* @param[in] needle pointer to string to search for.
|
||||
* @param[in] len length of @p haystack string. Also used as limit to
|
||||
* length of @p needle string.
|
||||
*
|
||||
* @return @p haystack, if @p needle is an empty string otherwise NULL, if
|
||||
* @p needle does not occur in @p haystack, or a pointer to the
|
||||
* first character of the first occurrence of @p needle.
|
||||
*/
|
||||
static char*
|
||||
strnstr(const char *haystack, const char *needle, size_t len)
|
||||
{
|
||||
size_t i;
|
||||
size_t needleLen;
|
||||
const char* needleEnd;
|
||||
|
||||
// strnlen is not part of the C standard and does not compile on some platforms,
|
||||
// use case is covered by memchr.
|
||||
needleEnd = (char *)memchr(needle, 0, len);
|
||||
if (needleEnd == needle)
|
||||
return (char *)haystack;
|
||||
|
||||
needleLen = len;
|
||||
if (needleEnd != NULL)
|
||||
needleLen = needleEnd - needle;
|
||||
|
||||
for (i = 0; i <= len - needleLen; i++)
|
||||
{
|
||||
if (haystack[0] == needle[0]
|
||||
&& strncmp(haystack, needle, needleLen) == 0)
|
||||
return (char *)haystack;
|
||||
haystack++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @internal
|
||||
* @~English
|
||||
* @brief Append the library's id to the KTXwriter value.
|
||||
*
|
||||
* @param[in] head pointer to the head of the hash list.
|
||||
* @param[in] writerEntry pointer to an existing KTXwriter entry.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_OUT_OF_MEMORY not enough memory for temporary strings.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* the length of the value of writerEntry and the
|
||||
* lib id being added is greater than the
|
||||
* maximum allowed.
|
||||
*/
|
||||
KTX_error_code
|
||||
appendLibId(ktxHashList* head, ktxHashListEntry* writerEntry)
|
||||
{
|
||||
KTX_error_code result;
|
||||
const char* id;
|
||||
const char* libVer;
|
||||
const char libIdIntro[] = " / libktx ";
|
||||
size_t idLen, libIdLen;
|
||||
|
||||
if (writerEntry) {
|
||||
ktx_uint32_t len;
|
||||
result = ktxHashListEntry_GetValue(writerEntry, &len, (void**)&id);
|
||||
idLen = len;
|
||||
} else {
|
||||
id = "Unidentified app";
|
||||
idLen = 17;
|
||||
}
|
||||
|
||||
// strnstr needed because KTXwriter values may not be NUL terminated.
|
||||
#if defined(EMPTY_LIBVER_WITH_UNIDENTIFIED_APP)
|
||||
// May be needed for patching some CTS files without changing their KTXwriter
|
||||
// metadata. Keep in case useful again.
|
||||
if (strnstr(id, "Unidentified app", idLen) != NULL) {
|
||||
libVer = "";
|
||||
} else
|
||||
#endif
|
||||
if (strnstr(id, "__default__", idLen) != NULL) {
|
||||
libVer = STR(LIBKTX_DEFAULT_VERSION);
|
||||
} else {
|
||||
libVer = STR(LIBKTX_VERSION);
|
||||
}
|
||||
// sizeof(libIdIntro) includes space for its terminating NUL which we will
|
||||
// overwrite so no need for +1 after strlen.
|
||||
libIdLen = sizeof(libIdIntro) + (ktx_uint32_t)strlen(libVer);
|
||||
char* libId = malloc(libIdLen);
|
||||
if (!libId)
|
||||
return KTX_OUT_OF_MEMORY;
|
||||
strncpy(libId, libIdIntro, libIdLen);
|
||||
strncpy(&libId[sizeof(libIdIntro)-1], libVer,
|
||||
libIdLen-(sizeof(libIdIntro)-1));
|
||||
|
||||
char* fullId = NULL;
|
||||
if (strnstr(id, libId, idLen) != NULL) {
|
||||
// This lib id is already in the writer value.
|
||||
result = KTX_SUCCESS;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
const char* libVerPos = strnstr(id, libIdIntro, idLen);
|
||||
if (libVerPos != NULL) {
|
||||
// There is a libktx version but not the current version.
|
||||
idLen = libVerPos - id;
|
||||
} else if (id[idLen-1] == '\0') {
|
||||
idLen--;
|
||||
}
|
||||
|
||||
size_t fullIdLen = idLen + strlen(libId) + 1;
|
||||
if (fullIdLen > UINT_MAX) {
|
||||
result = KTX_INVALID_OPERATION;
|
||||
goto cleanup;
|
||||
}
|
||||
fullId = malloc(fullIdLen);
|
||||
if (!fullId) {
|
||||
result = KTX_OUT_OF_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
strncpy(fullId, id, idLen);
|
||||
strncpy(&fullId[idLen], libId, libIdLen);
|
||||
assert(fullId[fullIdLen-1] == '\0');
|
||||
|
||||
ktxHashList_DeleteEntry(head, writerEntry);
|
||||
result = ktxHashList_AddKVPair(head, KTX_WRITER_KEY,
|
||||
(ktx_uint32_t)fullIdLen, fullId);
|
||||
cleanup:
|
||||
free(libId);
|
||||
free(fullId);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2 @private
|
||||
* @~English
|
||||
* @brief Set image for level, layer, faceSlice from a ktxStream source.
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in] level mip level of the image to set.
|
||||
* @param[in] layer array layer of the image to set.
|
||||
* @param[in] faceSlice cube map face or depth slice of the image to set or
|
||||
* KTX_FACESLICE_WHOLE_LEVEL to set the entire level.
|
||||
* @param[in] src ktxStream pointer to the source.
|
||||
* @param[in] srcSize size of the source image in bytes.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This or @p src is NULL.
|
||||
* @exception KTX_INVALID_VALUE @p srcSize != the expected image size for the
|
||||
* specified level, layer & faceSlice.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* No storage was allocated when the texture was
|
||||
* created.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_setImageFromStream(ktxTexture2* This, ktx_uint32_t level,
|
||||
ktx_uint32_t layer, ktx_uint32_t faceSlice,
|
||||
ktxStream* src, ktx_size_t srcSize)
|
||||
{
|
||||
ktx_size_t imageByteLength;
|
||||
ktx_size_t imageByteOffset;
|
||||
ktx_error_code_e result;
|
||||
|
||||
if (!This || !src)
|
||||
return KTX_INVALID_VALUE;
|
||||
|
||||
if (!This->pData)
|
||||
return KTX_INVALID_OPERATION;
|
||||
|
||||
if (faceSlice == KTX_FACESLICE_WHOLE_LEVEL) {
|
||||
result = ktxTexture_GetImageOffset(ktxTexture(This), level, layer, 0, &imageByteOffset);
|
||||
if (result != KTX_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
imageByteLength = ktxTexture_calcLevelSize(ktxTexture(This), level, KTX_FORMAT_VERSION_TWO);
|
||||
} else {
|
||||
result = ktxTexture_GetImageOffset(ktxTexture(This), level, layer, faceSlice, &imageByteOffset);
|
||||
if (result != KTX_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
imageByteLength = ktxTexture_GetImageSize(ktxTexture(This), level);
|
||||
}
|
||||
|
||||
if (srcSize != imageByteLength)
|
||||
return KTX_INVALID_OPERATION;
|
||||
// The above will catch a flagrantly invalid srcSize. This is an
|
||||
// additional check of the internal calculations.
|
||||
assert (imageByteOffset + srcSize <= This->dataSize);
|
||||
|
||||
/* Can copy whole image at once */
|
||||
src->read(src, This->pData + imageByteOffset, srcSize);
|
||||
return KTX_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Set image for level, layer, faceSlice from a stdio stream source.
|
||||
*
|
||||
* Uncompressed images read from the stream are expected to have their rows
|
||||
* tightly packed as is the norm for most image file formats. KTX 2 also requires
|
||||
* tight packing this function does not add any padding.
|
||||
*
|
||||
* Level, layer, faceSlice rather than offset are specified to enable some
|
||||
* validation.
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in] level mip level of the image to set.
|
||||
* @param[in] layer array layer of the image to set.
|
||||
* @param[in] faceSlice cube map face or depth slice of the image to set or
|
||||
* KTX_FACESLICE_WHOLE_LEVEL to set the entire level.
|
||||
* @param[in] src stdio stream pointer to the source.
|
||||
* @param[in] srcSize size of the source image in bytes.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This or @p src is NULL.
|
||||
* @exception KTX_INVALID_VALUE @p srcSize != the expected image size for the
|
||||
* specified level, layer & faceSlice.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* No storage was allocated when the texture was
|
||||
* created.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_SetImageFromStdioStream(ktxTexture2* This, ktx_uint32_t level,
|
||||
ktx_uint32_t layer, ktx_uint32_t faceSlice,
|
||||
FILE* src, ktx_size_t srcSize)
|
||||
{
|
||||
ktxStream srcstr;
|
||||
KTX_error_code result;
|
||||
|
||||
result = ktxFileStream_construct(&srcstr, src, KTX_FALSE);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
result = ktxTexture2_setImageFromStream(This, level, layer, faceSlice,
|
||||
&srcstr, srcSize);
|
||||
ktxFileStream_destruct(&srcstr);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Set image for level, layer, faceSlice from an image in memory.
|
||||
*
|
||||
* Uncompressed images in memory are expected to have their rows tightly packed
|
||||
* as is the norm for most image file formats. KTX 2 also requires
|
||||
* tight packing this function does not add any padding.
|
||||
*
|
||||
* Level, layer, faceSlice rather than offset are specified to enable some
|
||||
* validation.
|
||||
*
|
||||
* @note The caller is responsible for freeing the original image memory
|
||||
* referred to by @p src.
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in] level mip level of the image to set.
|
||||
* @param[in] layer array layer of the image to set.
|
||||
* @param[in] faceSlice cube map face or depth slice of the image to set or
|
||||
* KTX_FACESLICE_WHOLE_LEVEL to set the entire level.
|
||||
* @param[in] src pointer to the image source in memory.
|
||||
* @param[in] srcSize size of the source image in bytes.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This or @p src is NULL.
|
||||
* @exception KTX_INVALID_VALUE @p srcSize != the expected image size for the
|
||||
* specified level, layer & faceSlice.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* No storage was allocated when the texture was
|
||||
* created.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_SetImageFromMemory(ktxTexture2* This, ktx_uint32_t level,
|
||||
ktx_uint32_t layer, ktx_uint32_t faceSlice,
|
||||
const ktx_uint8_t* src, ktx_size_t srcSize)
|
||||
{
|
||||
ktxStream srcstr;
|
||||
KTX_error_code result;
|
||||
|
||||
result = ktxMemStream_construct_ro(&srcstr, src, srcSize);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
result = ktxTexture2_setImageFromStream(This, level, layer, faceSlice,
|
||||
&srcstr, srcSize);
|
||||
ktxMemStream_destruct(&srcstr);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if defined(TestNoMetadata)
|
||||
// Only so texturetests can test loading of files without any metadata.
|
||||
ktx_bool_t __disableWriterMetadata__ = KTX_FALSE;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Write a ktxTexture object to a ktxStream in KTX format.
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in] dststr destination ktxStream.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This or @p dststr is NULL.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* The ktxTexture does not contain any image data.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* Both kvDataHead and kvData are set in the
|
||||
* ktxTexture
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* The length of the already set writerId metadata
|
||||
* plus the library's version id exceeds the
|
||||
* maximum allowed.
|
||||
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
|
||||
* the system.
|
||||
* @exception KTX_FILE_WRITE_ERROR
|
||||
* An error occurred while writing the file.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_WriteToStream(ktxTexture2* This, ktxStream* dststr)
|
||||
{
|
||||
DECLARE_PRIVATE(ktxTexture2);
|
||||
KTX_header2 header = { .identifier = KTX2_IDENTIFIER_REF };
|
||||
KTX_error_code result;
|
||||
ktx_uint32_t kvdLen;
|
||||
ktx_uint8_t* pKvd;
|
||||
ktx_uint32_t align8PadLen = 0;
|
||||
ktx_uint64_t sgdLen;
|
||||
ktx_uint32_t initialLevelPadLen;
|
||||
ktx_uint32_t levelIndexSize;
|
||||
ktx_uint64_t baseOffset;
|
||||
|
||||
if (!dststr) {
|
||||
return KTX_INVALID_VALUE;
|
||||
}
|
||||
|
||||
if (This->pData == NULL)
|
||||
return KTX_INVALID_OPERATION;
|
||||
|
||||
header.vkFormat = This->vkFormat;
|
||||
header.typeSize = This->_protected->_typeSize;
|
||||
header.pixelWidth = This->baseWidth;
|
||||
header.pixelHeight = This->numDimensions > 1 ? This->baseHeight : 0;
|
||||
header.pixelDepth = This->numDimensions > 2 ? This->baseDepth : 0;
|
||||
header.layerCount = This->isArray ? This->numLayers : 0;
|
||||
assert (This->isCubemap ? This->numFaces == 6 : This->numFaces == 1);
|
||||
header.faceCount = This->numFaces;
|
||||
assert (This->generateMipmaps? This->numLevels == 1 : This->numLevels >= 1);
|
||||
header.levelCount = This->generateMipmaps ? 0 : This->numLevels;
|
||||
header.supercompressionScheme = This->supercompressionScheme;
|
||||
|
||||
levelIndexSize = sizeof(ktxLevelIndexEntry) * This->numLevels;
|
||||
|
||||
baseOffset = sizeof(header) + levelIndexSize;
|
||||
|
||||
header.dataFormatDescriptor.byteOffset = (uint32_t)baseOffset;
|
||||
header.dataFormatDescriptor.byteLength = *This->pDfd;
|
||||
baseOffset += header.dataFormatDescriptor.byteLength;
|
||||
|
||||
ktxHashListEntry* pEntry;
|
||||
// Check for invalid metadata.
|
||||
for (pEntry = This->kvDataHead; pEntry != NULL; pEntry = ktxHashList_Next(pEntry)) {
|
||||
unsigned int keyLen;
|
||||
char* key;
|
||||
|
||||
ktxHashListEntry_GetKey(pEntry, &keyLen, &key);
|
||||
if (strncasecmp(key, "KTX", 3) == 0) {
|
||||
ktx_uint32_t i;
|
||||
const char* knownKeys[] = {
|
||||
"KTXcubemapIncomplete",
|
||||
"KTXorientation",
|
||||
"KTXglFormat",
|
||||
"KTXdxgiFormat__",
|
||||
"KTXmetalPixelFormat",
|
||||
"KTXswizzle",
|
||||
"KTXwriter",
|
||||
"KTXwriterScParams",
|
||||
"KTXastcDecodeMode",
|
||||
"KTXanimData"
|
||||
};
|
||||
if (strncmp(key, "ktx", 3) == 0)
|
||||
return KTX_INVALID_OPERATION;
|
||||
// Check for unrecognized KTX keys.
|
||||
for (i = 0; i < sizeof(knownKeys)/sizeof(char*); i++) {
|
||||
if (strcmp(key, knownKeys[i]) == 0)
|
||||
break;
|
||||
}
|
||||
if (i == sizeof(knownKeys)/sizeof(char*))
|
||||
return KTX_INVALID_OPERATION;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(TestNoMetadata)
|
||||
if (!__disableWriterMetadata__) {
|
||||
#endif
|
||||
pEntry = NULL;
|
||||
result = ktxHashList_FindEntry(&This->kvDataHead, KTX_WRITER_KEY,
|
||||
&pEntry);
|
||||
result = appendLibId(&This->kvDataHead, pEntry);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
#if defined(TestNoMetadata)
|
||||
}
|
||||
#endif
|
||||
|
||||
ktxHashList_Sort(&This->kvDataHead); // KTX2 requires sorted metadata.
|
||||
ktxHashList_Serialize(&This->kvDataHead, &kvdLen, &pKvd);
|
||||
header.keyValueData.byteOffset = kvdLen != 0 ? (uint32_t)baseOffset : 0;
|
||||
header.keyValueData.byteLength = kvdLen;
|
||||
baseOffset += kvdLen;
|
||||
|
||||
sgdLen = private->_sgdByteLength;
|
||||
if (sgdLen) {
|
||||
align8PadLen = _KTX_PAD8_LEN(baseOffset);
|
||||
baseOffset += align8PadLen;
|
||||
}
|
||||
|
||||
header.supercompressionGlobalData.byteOffset = sgdLen != 0 ? baseOffset : 0;
|
||||
header.supercompressionGlobalData.byteLength = sgdLen;
|
||||
baseOffset += sgdLen;
|
||||
|
||||
initialLevelPadLen = _KTX_PADN_LEN(This->_private->_requiredLevelAlignment,
|
||||
baseOffset);
|
||||
baseOffset += initialLevelPadLen;
|
||||
|
||||
// write header and indices
|
||||
result = dststr->write(dststr, &header, sizeof(header), 1);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
|
||||
// Create a copy of the level index with file-adjusted offsets and write it.
|
||||
ktxLevelIndexEntry* levelIndex
|
||||
= (ktxLevelIndexEntry*)malloc(levelIndexSize);
|
||||
if (!levelIndex)
|
||||
return KTX_OUT_OF_MEMORY;
|
||||
for (ktx_uint32_t level = 0; level < This->numLevels; level++) {
|
||||
levelIndex[level].byteLength = private->_levelIndex[level].byteLength;
|
||||
levelIndex[level].uncompressedByteLength
|
||||
= private->_levelIndex[level].uncompressedByteLength;
|
||||
levelIndex[level].byteOffset = private->_levelIndex[level].byteOffset;
|
||||
levelIndex[level].byteOffset += baseOffset;
|
||||
}
|
||||
result = dststr->write(dststr, levelIndex, levelIndexSize, 1);
|
||||
free(levelIndex);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
|
||||
// write data format descriptor
|
||||
result = dststr->write(dststr, This->pDfd, 1, *This->pDfd);
|
||||
|
||||
// write keyValueData
|
||||
if (kvdLen != 0) {
|
||||
assert(pKvd != NULL);
|
||||
|
||||
result = dststr->write(dststr, pKvd, 1, kvdLen);
|
||||
free(pKvd);
|
||||
if (result != KTX_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
char padding[32] = { 0 };
|
||||
// write supercompressionGlobalData & sgdPadding
|
||||
if (private->_sgdByteLength != 0) {
|
||||
if (align8PadLen) {
|
||||
result = dststr->write(dststr, padding, 1, align8PadLen);
|
||||
if (result != KTX_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result = dststr->write(dststr, private->_supercompressionGlobalData,
|
||||
1, private->_sgdByteLength);
|
||||
if (result != KTX_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (initialLevelPadLen) {
|
||||
result = dststr->write(dststr, padding, 1, initialLevelPadLen);
|
||||
if (result != KTX_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// write the image data
|
||||
for (ktx_int32_t level = This->numLevels-1; level >= 0 && result == KTX_SUCCESS; --level)
|
||||
{
|
||||
ktx_uint64_t srcLevelOffset, levelSize;
|
||||
#define DUMP_IMAGE 0
|
||||
#if defined(DEBUG) || DUMP_IMAGE
|
||||
ktx_size_t pos;
|
||||
#endif
|
||||
|
||||
#if defined(DEBUG)
|
||||
result = dststr->getpos(dststr, (ktx_off_t*)&pos);
|
||||
// Could fail if stdout is a pipe
|
||||
if (result == KTX_SUCCESS)
|
||||
assert(pos == private->_levelIndex[level].byteOffset + baseOffset);
|
||||
else
|
||||
assert(result == KTX_FILE_ISPIPE);
|
||||
#endif
|
||||
|
||||
srcLevelOffset = ktxTexture2_levelDataOffset(This, level);
|
||||
levelSize = private->_levelIndex[level].byteLength;
|
||||
|
||||
#if DUMP_IMAGE
|
||||
if (!This->isCompressed) {
|
||||
for (layer = 0; layer < This->numLayers; layer++) {
|
||||
ktx_uint32_t faceSlice;
|
||||
for (faceSlice = 0; faceSlice < numImages; faceSlice++) {
|
||||
dststr->getsize(dststr, &pos);
|
||||
fprintf(stdout, "Writing level %d, layer %d, faceSlice %d to baseOffset %#zx\n",
|
||||
level, layer, faceSlice, pos);
|
||||
for (uint32_t y = 0; y < (This->baseHeight >> level); y++) {
|
||||
for (uint32_t x = 0; x < rowBytes; x++) {
|
||||
fprintf(stdout, "%#x, ",
|
||||
*(This->pData + srcOffset + y * rowBytes + x));
|
||||
}
|
||||
fprintf(stdout, "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fprintf(stdout, "\n");
|
||||
#endif
|
||||
// Write entire level.
|
||||
result = dststr->write(dststr, This->pData + srcLevelOffset,
|
||||
levelSize, 1);
|
||||
if (result == KTX_SUCCESS && level > 0) { // No padding at end.
|
||||
ktx_uint32_t levelPadLen
|
||||
= _KTX_PADN_LEN(This->_private->_requiredLevelAlignment,
|
||||
levelSize);
|
||||
if (levelPadLen != 0)
|
||||
result = dststr->write(dststr, padding, 1, levelPadLen);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Write a ktxTexture object to a stdio stream in KTX format.
|
||||
*
|
||||
* Callers are strongly urged to include a KTXwriter item in the texture's
|
||||
* metadata. It can be added by code, similar to the following, prior to
|
||||
* calling this function.
|
||||
* @code
|
||||
* char writer[100];
|
||||
* snprintf(writer, sizeof(writer), "%s version %s", appName, appVer);
|
||||
* ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
|
||||
* (ktx_uint32_t)strlen(writer) + 1,
|
||||
* writer);
|
||||
* @endcode
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in] dstsstr destination stdio stream.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This or @p dstsstr is NULL.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* The ktxTexture does not contain any image data.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* Both kvDataHead and kvData are set in the
|
||||
* ktxTexture
|
||||
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
|
||||
* the system.
|
||||
* @exception KTX_FILE_WRITE_ERROR
|
||||
* An error occurred while writing the file.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_WriteToStdioStream(ktxTexture2* This, FILE* dstsstr)
|
||||
{
|
||||
ktxStream stream;
|
||||
KTX_error_code result = KTX_SUCCESS;
|
||||
|
||||
if (!This)
|
||||
return KTX_INVALID_VALUE;
|
||||
|
||||
result = ktxFileStream_construct(&stream, dstsstr, KTX_FALSE);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
|
||||
return ktxTexture2_WriteToStream(This, &stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Write a ktxTexture object to a named file in KTX format.
|
||||
*
|
||||
* The file name must be encoded in utf-8. On Windows convert unicode names
|
||||
* to utf-8 with @c WideCharToMultiByte(CP_UTF8, ...) before calling.
|
||||
*
|
||||
* Callers are strongly urged to include a KTXwriter item in the texture's
|
||||
* metadata. It can be added by code, similar to the following, prior to
|
||||
* calling this function.
|
||||
* @code
|
||||
* char writer[100];
|
||||
* snprintf(writer, sizeof(writer), "%s version %s", appName, appVer);
|
||||
* ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
|
||||
* (ktx_uint32_t)strlen(writer) + 1,
|
||||
* writer);
|
||||
* @endcode
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in] dstname destination file name.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This or @p dstname is NULL.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* The ktxTexture does not contain any image data.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* Both kvDataHead and kvData are set in the
|
||||
* ktxTexture
|
||||
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
|
||||
* the system.
|
||||
* @exception KTX_FILE_WRITE_ERROR
|
||||
* An error occurred while writing the file.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_WriteToNamedFile(ktxTexture2* This, const char* const dstname)
|
||||
{
|
||||
KTX_error_code result;
|
||||
FILE* dst;
|
||||
|
||||
if (!This)
|
||||
return KTX_INVALID_VALUE;
|
||||
|
||||
dst = ktxFOpenUTF8(dstname, "wb");
|
||||
if (dst) {
|
||||
result = ktxTexture2_WriteToStdioStream(This, dst);
|
||||
fclose(dst);
|
||||
} else
|
||||
result = KTX_FILE_OPEN_FAILED;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Write a ktxTexture object to block of memory in KTX format.
|
||||
*
|
||||
* Memory is allocated by the function and the caller is responsible for
|
||||
* freeing it.
|
||||
*
|
||||
* Callers are strongly urged to include a KTXwriter item in the texture's
|
||||
* metadata. It can be added by code, similar to the following, prior to
|
||||
* calling this function.
|
||||
* @code
|
||||
* char writer[100];
|
||||
* snprintf(writer, sizeof(writer), "%s version %s", appName, appVer);
|
||||
* ktxHashList_AddKVPair(&texture->kvDataHead, KTX_WRITER_KEY,
|
||||
* (ktx_uint32_t)strlen(writer) + 1,
|
||||
* writer);
|
||||
* @endcode
|
||||
*
|
||||
* @param[in] This pointer to the target ktxTexture object.
|
||||
* @param[in,out] ppDstBytes pointer to location to write the address of
|
||||
* the destination memory. The Application is
|
||||
* responsible for freeing this memory.
|
||||
* @param[in,out] pSize pointer to location to write the size in bytes of
|
||||
* the KTX data.
|
||||
*
|
||||
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
|
||||
*
|
||||
* @exception KTX_INVALID_VALUE @p This, @p ppDstBytes or @p pSize is NULL.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* The ktxTexture does not contain any image data.
|
||||
* @exception KTX_INVALID_OPERATION
|
||||
* Both kvDataHead and kvData are set in the
|
||||
* ktxTexture
|
||||
* @exception KTX_FILE_OVERFLOW The file exceeded the maximum size supported by
|
||||
* the system.
|
||||
* @exception KTX_FILE_WRITE_ERROR
|
||||
* An error occurred while writing the file.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_WriteToMemory(ktxTexture2* This,
|
||||
ktx_uint8_t** ppDstBytes, ktx_size_t* pSize)
|
||||
{
|
||||
struct ktxStream dststr;
|
||||
KTX_error_code result;
|
||||
ktx_size_t strSize;
|
||||
|
||||
if (!This || !ppDstBytes || !pSize)
|
||||
return KTX_INVALID_VALUE;
|
||||
|
||||
*ppDstBytes = NULL;
|
||||
|
||||
result = ktxMemStream_construct(&dststr, KTX_FALSE);
|
||||
if (result != KTX_SUCCESS)
|
||||
return result;
|
||||
|
||||
result = ktxTexture2_WriteToStream(This, &dststr);
|
||||
if(result != KTX_SUCCESS)
|
||||
{
|
||||
ktxMemStream_destruct(&dststr);
|
||||
return result;
|
||||
}
|
||||
|
||||
ktxMemStream_getdata(&dststr, ppDstBytes);
|
||||
dststr.getsize(&dststr, &strSize);
|
||||
*pSize = (GLsizei)strSize;
|
||||
/* This function does not free the memory pointed at by the
|
||||
* value obtained from ktxMemStream_getdata() thanks to the
|
||||
* KTX_FALSE passed to the constructor above.
|
||||
*/
|
||||
ktxMemStream_destruct(&dststr);
|
||||
return KTX_SUCCESS;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Deflate the data in a ktxTexture2 object using Zstandard.
|
||||
*
|
||||
* The texture's levelIndex, dataSize, DFD, data pointer, and supercompressionScheme will
|
||||
* all be updated after successful deflation to reflect the deflated data.
|
||||
*
|
||||
* @param[in] This pointer to the ktxTexture2 object of interest.
|
||||
* @param[in] compressionLevel set speed vs compression ratio trade-off. Values
|
||||
* between 1 and 22 are accepted. The lower the level the faster. Values
|
||||
* above 20 should be used with caution as they require more memory.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_DeflateZstd(ktxTexture2* This, ktx_uint32_t compressionLevel)
|
||||
{
|
||||
ktx_uint32_t levelIndexByteLength =
|
||||
This->numLevels * sizeof(ktxLevelIndexEntry);
|
||||
ktx_uint8_t* workBuf;
|
||||
ktx_uint8_t* cmpData;
|
||||
ktx_size_t dstRemainingByteLength = 0;
|
||||
ktx_size_t byteLengthCmp = 0;
|
||||
ktx_size_t levelOffset = 0;
|
||||
ktxLevelIndexEntry* cindex = This->_private->_levelIndex;
|
||||
ktxLevelIndexEntry* nindex;
|
||||
ktx_uint8_t* pCmpDst;
|
||||
ktx_error_code_e result;
|
||||
|
||||
ZSTD_CCtx* cctx = ZSTD_createCCtx();
|
||||
if (cctx == NULL)
|
||||
return KTX_OUT_OF_MEMORY;
|
||||
|
||||
if (This->supercompressionScheme != KTX_SS_NONE)
|
||||
return KTX_INVALID_OPERATION;
|
||||
|
||||
// On rare occasions the deflated data can be a few bytes larger than
|
||||
// the source data. Calculating the dst buffer size using
|
||||
// ZSTD_compressBound provides a suitable size plus compression is said
|
||||
// to run faster when the dst buffer is >= compressBound.
|
||||
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
|
||||
dstRemainingByteLength += ZSTD_compressBound(cindex[level].byteLength);
|
||||
}
|
||||
|
||||
workBuf = malloc(dstRemainingByteLength + levelIndexByteLength);
|
||||
if (workBuf == NULL) {
|
||||
result = KTX_OUT_OF_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
nindex = (ktxLevelIndexEntry*)workBuf;
|
||||
pCmpDst = &workBuf[levelIndexByteLength];
|
||||
|
||||
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
|
||||
size_t levelByteLengthCmp =
|
||||
ZSTD_compressCCtx(cctx, pCmpDst + levelOffset,
|
||||
dstRemainingByteLength,
|
||||
&This->pData[cindex[level].byteOffset],
|
||||
cindex[level].byteLength,
|
||||
compressionLevel);
|
||||
if (ZSTD_isError(levelByteLengthCmp)) {
|
||||
free(workBuf);
|
||||
ZSTD_ErrorCode error = ZSTD_getErrorCode(levelByteLengthCmp);
|
||||
switch(error) {
|
||||
case ZSTD_error_parameter_outOfBound:
|
||||
result = KTX_INVALID_VALUE;
|
||||
goto cleanup;
|
||||
case ZSTD_error_dstSize_tooSmall:
|
||||
#ifdef DEBUG
|
||||
assert(false && "Deflate dstSize too small.");
|
||||
#else
|
||||
result = KTX_OUT_OF_MEMORY;
|
||||
goto cleanup;
|
||||
#endif
|
||||
case ZSTD_error_workSpace_tooSmall:
|
||||
#ifdef DEBUG
|
||||
assert(false && "Deflate workspace too small.");
|
||||
#else
|
||||
result = KTX_OUT_OF_MEMORY;
|
||||
goto cleanup;
|
||||
#endif
|
||||
case ZSTD_error_memory_allocation:
|
||||
result = KTX_OUT_OF_MEMORY;
|
||||
goto cleanup;
|
||||
default:
|
||||
// The remaining errors look like they should only
|
||||
// occur during decompression but just in case.
|
||||
#ifdef DEBUG
|
||||
assert(true);
|
||||
#else
|
||||
result = KTX_INVALID_OPERATION;
|
||||
goto cleanup;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
nindex[level].byteOffset = levelOffset;
|
||||
nindex[level].uncompressedByteLength = cindex[level].byteLength;
|
||||
nindex[level].byteLength = levelByteLengthCmp;
|
||||
byteLengthCmp += levelByteLengthCmp;
|
||||
levelOffset += levelByteLengthCmp;
|
||||
dstRemainingByteLength -= levelByteLengthCmp;
|
||||
}
|
||||
ZSTD_freeCCtx(cctx);
|
||||
|
||||
// Move the compressed data into a correctly sized buffer.
|
||||
cmpData = malloc(byteLengthCmp);
|
||||
if (cmpData == NULL) {
|
||||
free(workBuf);
|
||||
return KTX_OUT_OF_MEMORY;
|
||||
}
|
||||
// Now modify the texture.
|
||||
memcpy(cmpData, pCmpDst, byteLengthCmp); // Copy data to sized buffer.
|
||||
memcpy(cindex, nindex, levelIndexByteLength); // Update level index
|
||||
free(workBuf);
|
||||
free(This->pData);
|
||||
This->pData = cmpData;
|
||||
This->dataSize = byteLengthCmp;
|
||||
This->supercompressionScheme = KTX_SS_ZSTD;
|
||||
This->_private->_requiredLevelAlignment = 1;
|
||||
|
||||
return KTX_SUCCESS;
|
||||
|
||||
cleanup:
|
||||
ZSTD_freeCCtx(cctx);
|
||||
free(workBuf);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof ktxTexture2
|
||||
* @~English
|
||||
* @brief Deflate the data in a ktxTexture2 object using miniz (ZLIB).
|
||||
*
|
||||
* The texture's levelIndex, dataSize, DFD, data pointer, and supercompressionScheme will
|
||||
* all be updated after successful deflation to reflect the deflated data.
|
||||
*
|
||||
* @param[in] This pointer to the ktxTexture2 object of interest.
|
||||
* @param[in] compressionLevel set speed vs compression ratio trade-off. Values
|
||||
* between 1 and 9 are accepted. The lower the level the faster.
|
||||
*/
|
||||
KTX_error_code
|
||||
ktxTexture2_DeflateZLIB(ktxTexture2* This, ktx_uint32_t compressionLevel)
|
||||
{
|
||||
ktx_uint32_t levelIndexByteLength =
|
||||
This->numLevels * sizeof(ktxLevelIndexEntry);
|
||||
ktx_uint8_t* workBuf;
|
||||
ktx_uint8_t* cmpData;
|
||||
ktx_size_t dstRemainingByteLength = 0;
|
||||
ktx_size_t byteLengthCmp = 0;
|
||||
ktx_size_t levelOffset = 0;
|
||||
ktxLevelIndexEntry* cindex = This->_private->_levelIndex;
|
||||
ktxLevelIndexEntry* nindex;
|
||||
ktx_uint8_t* pCmpDst;
|
||||
|
||||
if (This->supercompressionScheme != KTX_SS_NONE)
|
||||
return KTX_INVALID_OPERATION;
|
||||
|
||||
// On rare occasions the deflated data can be a few bytes larger than
|
||||
// the source data. Calculating the dst buffer size using
|
||||
// mz_deflateBound provides a conservative size to account for that.
|
||||
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
|
||||
dstRemainingByteLength += ktxCompressZLIBBounds(cindex[level].byteLength);
|
||||
}
|
||||
|
||||
workBuf = malloc(dstRemainingByteLength + levelIndexByteLength);
|
||||
if (workBuf == NULL)
|
||||
return KTX_OUT_OF_MEMORY;
|
||||
nindex = (ktxLevelIndexEntry*)workBuf;
|
||||
pCmpDst = &workBuf[levelIndexByteLength];
|
||||
|
||||
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
|
||||
size_t levelByteLengthCmp = dstRemainingByteLength;
|
||||
KTX_error_code result = ktxCompressZLIBInt(pCmpDst + levelOffset,
|
||||
&levelByteLengthCmp,
|
||||
&This->pData[cindex[level].byteOffset],
|
||||
cindex[level].byteLength,
|
||||
compressionLevel);
|
||||
if (result != KTX_SUCCESS) {
|
||||
free(workBuf);
|
||||
return result;
|
||||
}
|
||||
|
||||
nindex[level].byteOffset = levelOffset;
|
||||
nindex[level].uncompressedByteLength = cindex[level].byteLength;
|
||||
nindex[level].byteLength = levelByteLengthCmp;
|
||||
byteLengthCmp += levelByteLengthCmp;
|
||||
levelOffset += levelByteLengthCmp;
|
||||
dstRemainingByteLength -= levelByteLengthCmp;
|
||||
}
|
||||
|
||||
// Move the compressed data into a correctly sized buffer.
|
||||
cmpData = malloc(byteLengthCmp);
|
||||
if (cmpData == NULL) {
|
||||
free(workBuf);
|
||||
return KTX_OUT_OF_MEMORY;
|
||||
}
|
||||
// Now modify the texture.
|
||||
memcpy(cmpData, pCmpDst, byteLengthCmp); // Copy data to sized buffer.
|
||||
memcpy(cindex, nindex, levelIndexByteLength); // Update level index
|
||||
free(workBuf);
|
||||
free(This->pData);
|
||||
This->pData = cmpData;
|
||||
This->dataSize = byteLengthCmp;
|
||||
This->supercompressionScheme = KTX_SS_ZLIB;
|
||||
This->_private->_requiredLevelAlignment = 1;
|
||||
|
||||
return KTX_SUCCESS;
|
||||
}
|
||||
|
||||
/** @} */
|
||||
Reference in New Issue
Block a user