Files
2026-06-14 19:09:18 +01:00

1106 lines
41 KiB
C++

/* -*- tab-width: 4; -*- */
/* vi: set sw=2 ts=4 expandtab: */
/*
* Copyright 2019-2020 The Khronos Group Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @internal
* @file
* @~English
*
* @brief Functions for supercompressing a texture with Basis Universal.
*
* This is where two worlds collide. Ugly!
*
* @author Mark Callow, github.com/MarkCallow
*/
#include <inttypes.h>
#include <stdlib.h>
#include <zstd.h>
#include <KHR/khr_df.h>
#include "ktx.h"
#include "ktxint.h"
#include "texture2.h"
#include "vkformat_enum.h"
#include "vk_format.h"
#include "basis_sgd.h"
#if (EMSCRIPTEN)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
#endif
#include "basisu/encoder/basisu_comp.h"
#if (EMSCRIPTEN)
#pragma clang diagnostic pop
#endif
#include "basisu/transcoder/basisu_file_headers.h"
#include "basisu/transcoder/basisu_transcoder.h"
#include "dfdutils/dfd.h"
using namespace basisu;
using namespace basist;
typedef struct ktxBasisParamsV1 {
ktx_uint32_t structSize;
ktx_uint32_t threadCount;
ktx_uint32_t compressionLevel;
ktx_uint32_t qualityLevel;
ktx_uint32_t maxEndpoints;
float endpointRDOThreshold;
ktx_uint32_t maxSelectors;
float selectorRDOThreshold;
ktx_bool_t normalMap;
ktx_bool_t separateRGToRGB_A;
ktx_bool_t preSwizzle;
ktx_bool_t noEndpointRDO;
ktx_bool_t noSelectorRDO;
} ktxBasisParamsV1;
enum swizzle_e {
R = 1,
G = 2,
B = 3,
A = 4,
ZERO = 5,
ONE = 6,
};
typedef void
(* PFNBUCOPYCB)(uint8_t* rgbadst, uint8_t* rgbasrc, uint32_t src_len,
ktx_size_t image_size, swizzle_e swizzle[4]);
// All callbacks expect source images to have no row padding and expect
// component size to be 8 bits.
static void
copy_rgba_to_rgba(uint8_t* rgbadst, uint8_t* rgbasrc, uint32_t,
ktx_size_t image_size, swizzle_e[4])
{
memcpy(rgbadst, rgbasrc, image_size);
}
// Copy rgb to rgba. No swizzle.
static void
copy_rgb_to_rgba(uint8_t* rgbadst, uint8_t* rgbsrc, uint32_t,
ktx_size_t image_size, swizzle_e[4])
{
for (ktx_size_t i = 0; i < image_size; i += 3) {
memcpy(rgbadst, rgbsrc, 3);
rgbadst[3] = 0xff; // Convince Basis there is no alpha.
rgbadst += 4; rgbsrc += 3;
}
}
// This is not static only so the unit tests can access it.
void
swizzle_to_rgba(uint8_t* rgbadst, uint8_t* rgbasrc, uint32_t src_len,
ktx_size_t image_size, swizzle_e swizzle[4])
{
for (ktx_size_t i = 0; i < image_size; i += src_len) {
for (uint32_t c = 0; c < 4; c++) {
switch (swizzle[c]) {
case R:
rgbadst[c] = rgbasrc[0];
break;
case G:
rgbadst[c] = rgbasrc[1];
break;
case B:
rgbadst[c] = rgbasrc[2];
break;
case A:
rgbadst[c] = rgbasrc[3];
break;
case ZERO:
rgbadst[c] = 0x00;
break;
case ONE:
rgbadst[c] = 0xff;
break;
default:
assert(false);
}
}
rgbadst +=4; rgbasrc += src_len;
}
}
#if 0
static void
swizzle_rgba_to_rgba(uint8_t* rgbadst, uint8_t* rgbasrc, ktx_size_t image_size,
swizzle_e swizzle[4])
{
for (ktx_size_t i = 0; i < image_size; i += 4) {
for (uint32_t c = 0; c < 4; c++) {
switch (swizzle[c]) {
case 0:
rgbadst[c] = rgbasrc[0];
break;
case 1:
rgbadst[c] = rgbasrc[1];
break;
case 2:
rgbadst[c] = rgbasrc[2];
break;
case 3:
rgbadst[c] = rgbasrc[3];
break;
case 4:
rgbadst[c] = 0x00;
break;
case 5:
rgbadst[i+c] = 0xff;
break;
default:
assert(false);
}
}
rgbadst +=4; rgbasrc += 4;
}
}
static void
swizzle_rgb_to_rgba(uint8_t* rgbadst, uint8_t* rgbsrc, ktx_size_t image_size,
swizzle_e swizzle[4])
{
for (ktx_size_t i = 0; i < image_size; i += 3) {
for (uint32_t c = 0; c < 3; c++) {
switch (swizzle[c]) {
case 0:
rgbadst[c] = rgbsrc[0];
break;
case 1:
rgbadst[c] = rgbsrc[i];
break;
case 2:
rgbadst[c] = rgbsrc[2];
break;
case 3:
assert(false); // Shouldn't happen for an RGB texture.
break;
case 4:
rgbadst[c] = 0x00;
break;
case 5:
rgbadst[c] = 0xff;
break;
default:
assert(false);
}
}
rgbadst +=4; rgbsrc += 3;
}
}
static void
swizzle_rg_to_rgb_a(uint8_t* rgbadst, uint8_t* rgsrc, ktx_size_t image_size,
swizzle_e swizzle[4])
{
for (ktx_size_t i = 0; i < image_size; i += 2) {
for (uint32_t c = 0; c < 2; c++) {
switch (swizzle[c]) {
case 0:
rgbadst[c] = rgsrc[0];
break;
case 1:
rgbadst[c] = rgsrc[1];
break;
case 2:
assert(false); // Shouldn't happen for an RG texture.
break;
case 3:
assert(false); // Shouldn't happen for an RG texture.
break;
case 4:
rgbadst[c] = 0x00;
break;
case 5:
rgbadst[c] = 0xff;
break;
default:
assert(false);
}
}
}
}
#endif
// Rewrite DFD changing it to unsized. Account for the Basis compressor
// not including an all 1's alpha channel, which would have been removed before
// encoding and supercompression, by using hasAlpha.
static KTX_error_code
ktxTexture2_rewriteDfd4BasisLzETC1S(ktxTexture2* This,
alpha_content_e alphaContent,
bool isLuminance,
swizzle_e swizzle[4])
{
uint32_t* cdfd = This->pDfd;
uint32_t* cbdb = cdfd + 1;
uint32_t newSampleCount = alphaContent != eNone ? 2 : 1;
uint32_t ndbSize = KHR_DF_WORD_SAMPLESTART
+ newSampleCount * KHR_DF_WORD_SAMPLEWORDS;
ndbSize *= sizeof(uint32_t);
uint32_t ndfdSize = ndbSize + 1 * sizeof(uint32_t);
uint32_t* ndfd = (uint32_t *)malloc(ndfdSize);
uint32_t* nbdb = ndfd + 1;
if (!ndfd)
return KTX_OUT_OF_MEMORY;
*ndfd = ndfdSize;
KHR_DFDSETVAL(nbdb, VENDORID, KHR_DF_VENDORID_KHRONOS);
KHR_DFDSETVAL(nbdb, DESCRIPTORTYPE, KHR_DF_KHR_DESCRIPTORTYPE_BASICFORMAT);
KHR_DFDSETVAL(nbdb, VERSIONNUMBER, KHR_DF_VERSIONNUMBER_LATEST);
KHR_DFDSETVAL(nbdb, DESCRIPTORBLOCKSIZE, ndbSize);
KHR_DFDSETVAL(nbdb, MODEL, KHR_DF_MODEL_ETC1S);
KHR_DFDSETVAL(nbdb, PRIMARIES, KHR_DFDVAL(cbdb, PRIMARIES));
KHR_DFDSETVAL(nbdb, TRANSFER, KHR_DFDVAL(cbdb, TRANSFER));
KHR_DFDSETVAL(nbdb, FLAGS, KHR_DFDVAL(cbdb, FLAGS));
nbdb[KHR_DF_WORD_TEXELBLOCKDIMENSION0] =
3 | (3 << KHR_DF_SHIFT_TEXELBLOCKDIMENSION1);
nbdb[KHR_DF_WORD_BYTESPLANE0] = 0; /* bytesPlane3..0 = 0 */
KHR_DFDSETVAL(nbdb, BYTESPLANE0, 8);
if (alphaContent != eNone)
KHR_DFDSETVAL(nbdb, BYTESPLANE1, 8);
nbdb[KHR_DF_WORD_BYTESPLANE4] = 0; /* bytesPlane7..5 = 0 */
for (uint32_t sample = 0; sample < newSampleCount; sample++) {
uint16_t channelId, bitOffset;
if (sample == 0) {
bitOffset = 0;
if (!isLuminance && swizzle
&& swizzle[0] == swizzle[1] && swizzle[1] == swizzle[2])
channelId = KHR_DF_CHANNEL_ETC1S_RRR;
else
channelId = KHR_DF_CHANNEL_ETC1S_RGB;
} else {
assert(sample == 1 && alphaContent != eNone);
bitOffset = 64;
if (alphaContent == eAlpha)
channelId = KHR_DF_CHANNEL_ETC1S_AAA;
else if (alphaContent == eGreen)
channelId = KHR_DF_CHANNEL_ETC1S_GGG;
else // This is just to quiet a compiler warning.
channelId = KHR_DF_CHANNEL_ETC1S_RGB;
}
KHR_DFDSETSVAL(nbdb, sample, CHANNELID, channelId);
KHR_DFDSETSVAL(nbdb, sample, QUALIFIERS, 0);
KHR_DFDSETSVAL(nbdb, sample, SAMPLEPOSITION_ALL, 0);
KHR_DFDSETSVAL(nbdb, sample, BITOFFSET, bitOffset);
KHR_DFDSETSVAL(nbdb, sample, BITLENGTH, 63);
KHR_DFDSETSVAL(nbdb, sample, SAMPLELOWER, 0);
KHR_DFDSETSVAL(nbdb, sample, SAMPLEUPPER, UINT32_MAX);
}
This->pDfd = ndfd;
free(cdfd);
return KTX_SUCCESS;
}
static KTX_error_code
ktxTexture2_rewriteDfd4Uastc(ktxTexture2* This,
alpha_content_e alphaContent,
bool isLuminance,
swizzle_e swizzle[4])
{
uint32_t* cdfd = This->pDfd;
uint32_t* cbdb = cdfd + 1;
uint32_t ndbSize = KHR_DF_WORD_SAMPLESTART
+ 1 * KHR_DF_WORD_SAMPLEWORDS;
ndbSize *= sizeof(uint32_t);
uint32_t ndfdSize = ndbSize + 1 * sizeof(uint32_t);
uint32_t* ndfd = (uint32_t *)malloc(ndfdSize);
uint32_t* nbdb = ndfd + 1;
if (!ndfd)
return KTX_OUT_OF_MEMORY;
*ndfd = ndfdSize;
KHR_DFDSETVAL(nbdb, VENDORID, KHR_DF_VENDORID_KHRONOS);
KHR_DFDSETVAL(nbdb, DESCRIPTORTYPE, KHR_DF_KHR_DESCRIPTORTYPE_BASICFORMAT);
KHR_DFDSETVAL(nbdb, VERSIONNUMBER, KHR_DF_VERSIONNUMBER_LATEST);
KHR_DFDSETVAL(nbdb, DESCRIPTORBLOCKSIZE, ndbSize);
KHR_DFDSETVAL(nbdb, MODEL, KHR_DF_MODEL_UASTC);
KHR_DFDSETVAL(nbdb, PRIMARIES, KHR_DFDVAL(cbdb, PRIMARIES));
KHR_DFDSETVAL(nbdb, TRANSFER, KHR_DFDVAL(cbdb, TRANSFER));
KHR_DFDSETVAL(nbdb, FLAGS, KHR_DFDVAL(cbdb, FLAGS));
nbdb[KHR_DF_WORD_TEXELBLOCKDIMENSION0] =
3 | (3 << KHR_DF_SHIFT_TEXELBLOCKDIMENSION1);
nbdb[KHR_DF_WORD_BYTESPLANE0] = 16; /* bytesPlane0 = 16, bytesPlane3..1 = 0 */
nbdb[KHR_DF_WORD_BYTESPLANE4] = 0; /* bytesPlane7..5 = 0 */
// Set the data for our single sample
uint16_t channelId;
if (alphaContent == eAlpha) {
channelId = KHR_DF_CHANNEL_UASTC_RGBA;
} else if (alphaContent == eGreen) {
channelId = KHR_DF_CHANNEL_UASTC_RRRG;
} else if (swizzle && swizzle[2] == 0 && swizzle[3] == 1) {
channelId = KHR_DF_CHANNEL_UASTC_RG;
} else if (!isLuminance && swizzle
&& swizzle[0] == swizzle[1] && swizzle[1] == swizzle[2]) {
channelId = KHR_DF_CHANNEL_UASTC_RRR;
} else {
channelId = KHR_DF_CHANNEL_UASTC_RGB;
}
KHR_DFDSETSVAL(nbdb, 0, CHANNELID, channelId);
KHR_DFDSETSVAL(nbdb, 0, QUALIFIERS, 0);
KHR_DFDSETSVAL(nbdb, 0, SAMPLEPOSITION_ALL, 0);
KHR_DFDSETSVAL(nbdb, 0, BITOFFSET, 0);
KHR_DFDSETSVAL(nbdb, 0, BITLENGTH, 127);
KHR_DFDSETSVAL(nbdb, 0, SAMPLELOWER, 0);
KHR_DFDSETSVAL(nbdb, 0, SAMPLEUPPER, UINT32_MAX);
This->pDfd = ndfd;
free(cdfd);
return KTX_SUCCESS;
}
static bool basisuEncoderInitialized = false;
/**
* @memberof ktxTexture2
* @ingroup writer
* @~English
* @brief Encode and possibly Supercompress a KTX2 texture with uncompressed images.
*
* The images are either encoded to ETC1S block-compressed format and supercompressed
* with Basis LZ or they are encoded to UASTC block-compressed format. UASTC format is
* selected by setting the @c uastc field of @a params to @c KTX_TRUE. The encoded images
* replace the original images and the texture's fields including the DFD are modified to reflect the new
* state.
*
* Such textures must be transcoded to a desired target block compressed format
* before they can be uploaded to a GPU via a graphics API.
*
* @sa ktxTexture2_TranscodeBasis().
*
* @param[in] This pointer to the ktxTexture2 object of interest.
* @param[in] params pointer to Basis params object.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_OPERATION
* The texture's images are supercompressed.
* @exception KTX_INVALID_OPERATION
* The texture's images are in a block compressed
* format.
* @exception KTX_INVALID_OPERATION
* The texture image's format is a packed format
* (e.g. RGB565).
* @exception KTX_INVALID_OPERATION
* The texture image format's component size is
* not 8-bits.
* @exception KTX_INVALID_OPERATION
* @c normalMode is specified but the texture has
* only one component.
* @exception KTX_INVALID_OPERATION
* Both preSwizzle and and inputSwizzle are
* specified in @a params.
* @exception KTX_INVALID_OPERATION
* This->generateMipmaps is set.
* @exception KTX_OUT_OF_MEMORY Not enough memory to carry out compression.
*/
extern "C" KTX_error_code
ktxTexture2_CompressBasisEx(ktxTexture2* This, ktxBasisParams* params)
{
KTX_error_code result;
if (!params)
return KTX_INVALID_VALUE;
if (params->structSize != sizeof(struct ktxBasisParams))
return KTX_INVALID_VALUE;
if (This->generateMipmaps)
return KTX_INVALID_OPERATION;
if (This->supercompressionScheme != KTX_SS_NONE)
return KTX_INVALID_OPERATION; // Can't apply multiple schemes.
if (This->isCompressed)
return KTX_INVALID_OPERATION; // Only non-block compressed formats
// can be encoded into a Basis format.
if (This->_protected->_formatSize.flags & KTX_FORMAT_SIZE_PACKED_BIT)
return KTX_INVALID_OPERATION;
// Basic descriptor block begins after the total size field.
const uint32_t* BDB = This->pDfd+1;
uint32_t num_components, component_size;
getDFDComponentInfoUnpacked(This->pDfd, &num_components, &component_size);
if (component_size != 1)
return KTX_INVALID_OPERATION; // Basis must have 8-bit components.
if (num_components == 1 && params->normalMap)
return KTX_INVALID_OPERATION; // Not enough components.
if (This->pData == NULL) {
result = ktxTexture2_LoadImageData(This, NULL, 0);
if (result != KTX_SUCCESS)
return result;
}
if (!basisuEncoderInitialized) {
// force_serialization uses a mutex to serialize when multiple command
// queues per thread are used. We shouldn't need to worry about this.
// How to decide whether to use OpenCL?
basisu_encoder_init((BASISU_SUPPORT_OPENCL ? true : false)/*use_opencl*/
/*opencl_force_serialization = false*/);
//atexit(basisu_encoder_deinit);
basisuEncoderInitialized = true;
}
basis_compressor_params cparams;
cparams.m_read_source_images = false; // Don't read from source files.
cparams.m_write_output_basis_files = false; // Don't write output files.
cparams.m_status_output = params->verbose;
//
// Calculate number of images
//
uint32_t layersFaces = This->numLayers * This->numFaces;
uint32_t num_images = 0;
for (uint32_t level = 1; level <= This->numLevels; level++) {
// NOTA BENE: numFaces * depth is only reasonable because they can't
// both be > 1. I.e there are no 3d cubemaps.
num_images += layersFaces * MAX(This->baseDepth >> (level - 1), 1);
}
//
// Copy images into compressor parameters.
//
// Darn it! m_source_images is a vector of an internal image class which
// has its own array of RGBA-only pixels. Pending modifications to the
// basisu code we'll have to copy in the images.
cparams.m_source_images.resize(num_images);
basisu::vector<image>::iterator iit = cparams.m_source_images.begin();
// Since we have to copy the data into the vector image anyway do the
// separation here to avoid another loop over the image inside
// basis_compressor.
swizzle_e rg_to_rgba_mapping_etc1s[4] = { R, R, R, G };
swizzle_e rg_to_rgba_mapping_uastc[4] = { R, G, ZERO, ONE };
swizzle_e normal_map_mapping[4] = { R, R, R, G };
swizzle_e r_to_rgba_mapping[4] = { R, R, R, ONE };
swizzle_e meta_mapping[4] = {};
// All the above declarations need to stay here so they remain in scope
// until after the pixel copy loop as comp_mapping will ultimately point
// to one of them.
swizzle_e* comp_mapping = 0;
alpha_content_e alphaContent = eNone;
bool isLuminance = false;
std::string swizzleString = params->inputSwizzle;
if (params->preSwizzle) {
if (swizzleString.size() > 0) {
return KTX_INVALID_OPERATION;
}
ktxHashListEntry* swizzleEntry;
result = ktxHashList_FindEntry(&This->kvDataHead, KTX_SWIZZLE_KEY,
&swizzleEntry);
if (result == KTX_SUCCESS) {
ktx_uint32_t swizzleLen = 0;
char* swizzleStr = nullptr;
ktxHashListEntry_GetValue(swizzleEntry,
&swizzleLen, (void**)&swizzleStr);
// Remove the swizzle as it is no longer needed.
ktxHashList_DeleteEntry(&This->kvDataHead, swizzleEntry);
// Do it this way in case there is no NUL terminator.
swizzleString.resize(swizzleLen);
swizzleString.assign(swizzleStr, swizzleLen);
}
}
if (swizzleString.size() == 0) {
// Set appropriate default swizzle
if (params->normalMap) {
comp_mapping = normal_map_mapping;
alphaContent = eGreen;
} else if (num_components == 1) {
comp_mapping = r_to_rgba_mapping;
} else if (num_components == 2) {
if (params->uastc)
comp_mapping = rg_to_rgba_mapping_uastc;
else {
comp_mapping = rg_to_rgba_mapping_etc1s;
alphaContent = eGreen;
}
} else if (num_components == 4) {
alphaContent = eAlpha;
}
} else {
// Only set comp_mapping for cases we can't shortcut.
// If num_components < 3 we always swizzle so no shortcut there.
if (num_components < 3
|| (num_components == 3 && swizzleString.compare("rgb1"))
|| (num_components == 4 && swizzleString.compare("rgba"))) {
for (int i = 0; i < 4; i++) {
switch (swizzleString[i]) {
case 'r': meta_mapping[i] = R; break;
case 'g': meta_mapping[i] = G; break;
case 'b': meta_mapping[i] = B; break;
case 'a': meta_mapping[i] = A; break;
case '0': meta_mapping[i] = ZERO; break;
case '1': meta_mapping[i] = ONE; break;
}
}
comp_mapping = meta_mapping;
}
if (!params->normalMap) {
// An incoming swizzle of RRR1 or RRRG is assumed to be for a
// luminance texture. Set isLuminance so we can later distinguish
// this from the identical swizzle used for normal maps.
// cases for ETC1S.
if (meta_mapping[0] == meta_mapping[1]
&& meta_mapping[1] == meta_mapping[2]) {
// Same component in r, g & b
isLuminance = true;
}
if (meta_mapping[3] != ONE) {
alphaContent = eAlpha;
}
} else {
alphaContent = eGreen;
}
}
PFNBUCOPYCB copycb = copy_rgba_to_rgba; // Initialization is just to keep
// compilers happy
if (comp_mapping) {
copycb = swizzle_to_rgba;
} else {
switch (num_components) {
case 4: copycb = copy_rgba_to_rgba; break;
case 3: copycb = copy_rgb_to_rgba; break;
default: assert(false);
}
}
// NOTA BENE: It is advantageous for Basis LZ compression to order
// mipmap levels from largest to smallest.
for (uint32_t level = 0; level < This->numLevels; level++) {
uint32_t width = MAX(1, This->baseWidth >> level);
uint32_t height = MAX(1, This->baseHeight >> level);
uint32_t depth = MAX(1, This->baseDepth >> level);
ktx_size_t image_size = ktxTexture2_GetImageSize(This, level);
for (uint32_t layer = 0; layer < This->numLayers; layer++) {
uint32_t faceSlices = This->numFaces == 1 ? depth : This->numFaces;
for (ktx_uint32_t slice = 0; slice < faceSlices; slice++) {
ktx_size_t offset;
ktxTexture2_GetImageOffset(This, level, layer, slice, &offset);
iit->resize(width, height);
copycb((uint8_t*)iit->get_ptr(), This->pData + offset,
num_components, image_size,
comp_mapping);
++iit;
}
}
}
free(This->pData); // No longer needed. Reduce memory footprint.
This->pData = NULL;
This->dataSize = 0;
//
// Setup rest of compressor parameters
//
ktx_uint32_t threadCount = params->threadCount;
if (threadCount < 1)
threadCount = 1;
job_pool jpool(threadCount);
cparams.m_pJob_pool = &jpool;
#if BASISU_SUPPORT_SSE
bool prevSSESupport = g_cpu_supports_sse41;
if (params->noSSE)
g_cpu_supports_sse41 = false;
#endif
ktx_uint32_t transfer = KHR_DFDVAL(BDB, TRANSFER);
if (transfer == KHR_DF_TRANSFER_SRGB)
cparams.m_perceptual = true;
else
cparams.m_perceptual = false;
cparams.m_mip_gen = false; // We provide the mip levels.
cparams.m_uastc = params->uastc;
if (params->uastc) {
cparams.m_pack_uastc_flags = params->uastcFlags;
if (params->uastcRDO) {
cparams.m_rdo_uastc = true;
if (params->uastcRDOQualityScalar > 0.0f) {
cparams.m_rdo_uastc_quality_scalar =
params->uastcRDOQualityScalar;
}
if (params->uastcRDODictSize > 0) {
cparams.m_rdo_uastc_dict_size = params->uastcRDODictSize;
}
if (params->uastcRDOMaxSmoothBlockErrorScale > 0) {
cparams.m_rdo_uastc_max_smooth_block_error_scale =
params->uastcRDOMaxSmoothBlockErrorScale;
}
if (params->uastcRDOMaxSmoothBlockStdDev > 0) {
cparams.m_rdo_uastc_smooth_block_max_std_dev =
params->uastcRDOMaxSmoothBlockStdDev;
}
cparams.m_rdo_uastc_favor_simpler_modes_in_rdo_mode =
!params->uastcRDODontFavorSimplerModes;
cparams.m_rdo_uastc_favor_simpler_modes_in_rdo_mode =
!params->uastcRDONoMultithreading;
}
} else {
// ETC1S-related params.
// Explicit specification is required as 0 is a valid value
// in the basis_compressor leaving us without a good way to
// indicate the parameter has not been set by the caller. (If we
// leave m_compression_level unset it will default to 1. We don't
// want the default to differ from `basisu` so 0 can't be the default.
cparams.m_compression_level = params->compressionLevel;
// There's no default for m_quality_level. `basisu` tool overrides
// any explicit m_{endpoint,selector}_clusters settings with those
// calculated from m_quality_level, if the user set that option. On
// the other hand the basis_compressor overrides the values of
// m_{endpoint,selector}_rdo_thresh calculated from m_quality_level
// with explicit settings made by the user. Note that, unlike the
// first pair where both have to be set, each of the second pair
// independently override the value for it calculated from
// m_quality_level.
//
// This is confusing for the user and tricky to document clearly.
// Therefore we override qualityLevel if both of max{Endpoint,Selector}s
// have been set so both sets of parameters are treated the same,
// except that intentionally we require the caller to have set both
// of max{Endpoint,Selector}s
if (params->maxEndpoints && params->maxSelectors) {
cparams.m_max_endpoint_clusters = params->maxEndpoints;
cparams.m_max_selector_clusters = params->maxSelectors;
// cparams.m_quality_level = -1; // Default setting.
} else if (params->qualityLevel != 0) {
cparams.m_max_endpoint_clusters = 0;
cparams.m_max_selector_clusters = 0;
cparams.m_quality_level = params->qualityLevel;
} else {
cparams.m_max_endpoint_clusters = 0;
cparams.m_max_selector_clusters = 0;
cparams.m_quality_level = 128;
}
if (params->endpointRDOThreshold > 0)
cparams.m_endpoint_rdo_thresh = params->endpointRDOThreshold;
if (params->selectorRDOThreshold > 0)
cparams.m_selector_rdo_thresh = params->selectorRDOThreshold;
if (params->normalMap) {
cparams.m_no_endpoint_rdo = true;
cparams.m_no_selector_rdo = true;
} else {
cparams.m_no_endpoint_rdo = params->noEndpointRDO;
cparams.m_no_selector_rdo = params->noSelectorRDO;
}
}
// Flip images across Y axis
// cparams.m_y_flip = false; // Let tool, e.g. toktx do its own yflip so
// ktxTexture is consistent.
// Output debug information during compression
//cparams.m_debug = true;
// m_debug_images is pretty slow
//cparams.m_debug_images = true;
// Split the R channel to RGB and the G channel to alpha. We do the
// seperation in this func (see above) so leave this at its default, false.
//bool_param<false> m_seperate_rg_to_color_alpha;
// m_userdata0, m_userdata1 go directly into the .basis file header.
// No need to set.
if (This->isVideo) {
// Encoder uses this to decide whether to create p-frames.
cparams.m_tex_type = cBASISTexTypeVideoFrames;
// cparams.m_us_per_frame & m_framerate are not used by
// the encoder, except to write the values into the .basis file header,
// so no point in setting them.
} else {
// Set to cBASISTexType2D as any other setting is likely to cause
// validity checks that the encoder performs on its results, to
// fail. The checks only work properly when the encoder generates
// mipmaps tself and are oriented to ensuring the .basis file is
// sensible. Underlying compression works fine and we already know
// what level, layer and face/slice each image belongs too.
cparams.m_tex_type = cBASISTexType2D;
}
#define DUMP_BASIS_FILE 0
#if DUMP_BASIS_FILE
cparams.m_out_filename = "ktxtest.basis";
cparams.m_write_output_basis_files = true;
#endif
#define DEBUG_ENCODER 0
#if DEBUG_ENCODER
cparams.m_debug = true;
g_debug_printf = true;
#endif
basis_compressor c;
// As we don't use it, file reading support has been removed from the
// BasisU code. Ensure we don't accidentally try to use it.
assert(cparams.m_read_source_images == false
&& "m_read_source_images must be false");
// init() only returns false if told to read source image files and the
// list of files is empty.
(void)c.init(cparams);
//enable_debug_printf(true);
basis_compressor::error_code ec = c.process();
if (ec != basis_compressor::cECSuccess) {
// We should be sending valid 2d arrays, cubemaps or video ...
assert(ec != basis_compressor::cECFailedValidating);
// Do something sensible with other errors
return KTX_INVALID_OPERATION;
#if 0
switch (ec) {
case basis_compressor::cECFailedReadingSourceImages:
{
error_printf("Compressor failed reading a source image!\n");
if (opts.m_individual)
exit_flag = false;
break;
}
case basis_compressor::cECFailedFrontEnd:
error_printf("Compressor frontend stage failed!\n");
break;
case basis_compressor::cECFailedFontendExtract:
error_printf("Compressor frontend data extraction failed!\n");
break;
case basis_compressor::cECFailedBackend:
error_printf("Compressor backend stage failed!\n");
break;
case basis_compressor::cECFailedCreateBasisFile:
error_printf("Compressor failed creating Basis file data!\n");
break;
case basis_compressor::cECFailedWritingOutput:
error_printf("Compressor failed writing to output Basis file!\n");
break;
default:
error_printf("basis_compress::process() failed!\n");
break;
}
}
return KTX_WHAT_ERROR?;
#endif
}
#if DUMP_BASIS_FILE
return KTX_UNSUPPORTED_FEATURE;
#endif
//
// Compression successful. Now we have to unpick the basis output and
// copy the info and images to This texture.
//
#if BASISU_SUPPORT_SSE
g_cpu_supports_sse41 = prevSSESupport;
#endif
const uint8_vec& bf = c.get_output_basis_file();
const basis_file_header& bfh = *reinterpret_cast<const basis_file_header*>(bf.data());
assert(bfh.m_total_images == num_images);
uint8_t* bgd = nullptr;
size_t bgd_size;
uint32_t image_data_size = 0;
ktxTexture2_private& priv = *This->_private;
uint32_t base_offset = bfh.m_slice_desc_file_ofs;
const basis_slice_desc* slice
= reinterpret_cast<const basis_slice_desc*>(&bf[base_offset]);
std::vector<uint32_t> level_file_offsets(This->numLevels);
if (params->uastc) {
for (uint32_t level = 0; level < This->numLevels; level++) {
uint32_t depth = MAX(1, This->baseDepth >> level);
uint32_t levelByteLength = 0;
uint32_t levelImageCount = This->numLayers * This->numFaces * depth;
level_file_offsets[level] = slice->m_file_ofs;
for (uint32_t image = 0; image < levelImageCount; image++, slice++) {
image_data_size += slice->m_file_size;
levelByteLength += slice->m_file_size;
}
priv._levelIndex[level].byteLength = levelByteLength;
priv._levelIndex[level].uncompressedByteLength = levelByteLength;
}
} else {
//
// Allocate supercompression global data and write its header.
//
uint32_t image_desc_size = sizeof(ktxBasisLzEtc1sImageDesc);
bgd_size = sizeof(ktxBasisLzGlobalHeader)
+ image_desc_size * num_images
+ bfh.m_endpoint_cb_file_size + bfh.m_selector_cb_file_size
+ bfh.m_tables_file_size;
bgd = new ktx_uint8_t[bgd_size];
ktxBasisLzGlobalHeader& bgdh = *reinterpret_cast<ktxBasisLzGlobalHeader*>(bgd);
bgdh.endpointCount = (uint16_t)bfh.m_total_endpoints;
bgdh.endpointsByteLength = bfh.m_endpoint_cb_file_size;
bgdh.selectorCount = (uint16_t)bfh.m_total_selectors;
bgdh.selectorsByteLength = bfh.m_selector_cb_file_size;
bgdh.tablesByteLength = bfh.m_tables_file_size;
bgdh.extendedByteLength = 0;
//
// Write the index of slice descriptions to the global data.
//
ktxBasisLzEtc1sImageDesc* kimages = BGD_ETC1S_IMAGE_DESCS(bgd);
// 3 things to remember about offsets:
// 1. levelIndex offsets at this point are relative to This->pData;
// 2. In the ktx image descriptors, slice offsets are relative to the
// start of the mip level;
// 3. basis_slice_desc offsets are relative to the end of the basis
// header. Hence base_offset set above is used to rebase offsets
// relative to the start of the slice data.
// Assumption here is that slices produced by the compressor are in the
// same order as we passed them in above, i.e. ordered by mip level.
// Note also that slice->m_level_index is always 0, unless the compressor
// generated mip levels, so essentially useless. Alpha slices are always
// the odd numbered slices.
uint32_t image = 0;
for (uint32_t level = 0; level < This->numLevels; level++) {
uint32_t depth = MAX(1, This->baseDepth >> level);
uint32_t level_byte_length = 0;
assert(!(slice->m_flags & cSliceDescFlagsHasAlpha));
level_file_offsets[level] = slice->m_file_ofs;
for (uint32_t layer = 0; layer < This->numLayers; layer++) {
uint32_t faceSlices = This->numFaces == 1 ? depth
: This->numFaces;
for (uint32_t faceSlice = 0; faceSlice < faceSlices; faceSlice++) {
level_byte_length += slice->m_file_size;
kimages[image].rgbSliceByteOffset = slice->m_file_ofs
- level_file_offsets[level];
kimages[image].rgbSliceByteLength = slice->m_file_size;
if (bfh.m_flags & cBASISHeaderFlagHasAlphaSlices) {
slice++;
level_byte_length += slice->m_file_size;
kimages[image].alphaSliceByteOffset =
slice->m_file_ofs - level_file_offsets[level];
kimages[image].alphaSliceByteLength =
slice->m_file_size;
} else {
kimages[image].alphaSliceByteOffset = 0;
kimages[image].alphaSliceByteLength = 0;
}
// Set the PFrame flag, inverse of the .basis IFrame flag.
if (This->isVideo) {
// Extract FrameIsIFrame
kimages[image].imageFlags =
(slice->m_flags & ~cSliceDescFlagsHasAlpha);
// Set our flag to the inverse.
kimages[image].imageFlags ^=
cSliceDescFlagsFrameIsIFrame;
} else {
kimages[image].imageFlags = 0;
}
slice++;
image++;
}
}
priv._levelIndex[level].byteLength = level_byte_length;
priv._levelIndex[level].uncompressedByteLength = 0;
image_data_size += level_byte_length;
}
//
// Copy the global code books & huffman tables to global data.
//
// Slightly sleazy but as image is now the last valid index in the
// slice description array plus 1, &kimages[image] points at the first
// byte where the endpoints, etc. must be written.
uint8_t* dstptr = reinterpret_cast<uint8_t*>(&kimages[image]);
// Copy the endpoints ...
memcpy(dstptr,
&bf[bfh.m_endpoint_cb_file_ofs],
bfh.m_endpoint_cb_file_size);
dstptr += bgdh.endpointsByteLength;
// selectors ...
memcpy(dstptr,
&bf[bfh.m_selector_cb_file_ofs],
bfh.m_selector_cb_file_size);
dstptr += bgdh.selectorsByteLength;
// and the huffman tables.
memcpy(dstptr,
&bf[bfh.m_tables_file_ofs],
bfh.m_tables_file_size);
assert((size_t)(dstptr + bgdh.tablesByteLength - bgd) <= bgd_size);
//
// We have a complete global data package.
//
priv._supercompressionGlobalData = bgd;
priv._sgdByteLength = bgd_size;
}
//
// Update This texture and copy compressed image data to it.
//
// Declare here so can use goto cleanup.
uint8_t* new_data = 0;
ktxFormatSize& formatSize = This->_protected->_formatSize;
uint64_t level_offset = 0;
// Since we've left m_check_for_alpha set and m_force_alpha unset in
// the compressor parameters, the basis encoder will have removed an input
// alpha channel, if every alpha pixel in every image is 255 prior to
// encoding and supercompression. The DFD needs to reflect the encoded data
// not the input texture. Override the alphacontent setting, if this has
// happened.
if ((bfh.m_flags & cBASISHeaderFlagHasAlphaSlices) == 0) {
alphaContent = eNone;
}
new_data = (uint8_t*) malloc(image_data_size);
if (!new_data) {
result = KTX_OUT_OF_MEMORY;
goto cleanup;
}
// Delayed modifying texture until here so it's after points of
// possible failure.
if (params->uastc) {
result = ktxTexture2_rewriteDfd4Uastc(This, alphaContent,
isLuminance,
comp_mapping);
if (result != KTX_SUCCESS) goto cleanup;
// Reflect this in the formatSize
ktxFormatSize_initFromDfd(&formatSize, This->pDfd);
// and the requiredLevelAlignment.
priv._requiredLevelAlignment = 4 * 4;
} else {
result = ktxTexture2_rewriteDfd4BasisLzETC1S(This, alphaContent,
isLuminance,
comp_mapping);
if (result != KTX_SUCCESS) goto cleanup;
This->supercompressionScheme = KTX_SS_BASIS_LZ;
// Reflect this in the formatSize
ktxFormatSize_initFromDfd(&formatSize, This->pDfd);
// and the requiredLevelAlignment.
priv._requiredLevelAlignment = 1;
}
This->vkFormat = VK_FORMAT_UNDEFINED;
This->isCompressed = KTX_TRUE;
// Block-compressed textures never need byte swapping so typeSize is 1.
assert(This->_protected->_typeSize == 1);
// Copy in the compressed image data.
This->pData = new_data;
This->dataSize = image_data_size;
for (int32_t level = This->numLevels - 1; level >= 0; level--) {
priv._levelIndex[level].byteOffset = level_offset;
// byteLength was set in loop above
memcpy(This->pData + level_offset,
&bf[level_file_offsets[level]],
priv._levelIndex[level].byteLength);
level_offset += _KTX_PADN(priv._requiredLevelAlignment,
priv._levelIndex[level].byteLength);
}
return KTX_SUCCESS;
cleanup:
if (bgd) {
delete bgd;
priv._supercompressionGlobalData = 0;
priv._sgdByteLength = 0;
}
if (new_data) free(new_data);
return result;
}
extern "C" KTX_API const ktx_uint32_t KTX_ETC1S_DEFAULT_COMPRESSION_LEVEL
= BASISU_DEFAULT_COMPRESSION_LEVEL;
/**
* @memberof ktxTexture2
* @ingroup writer
* @~English
* @brief Supercompress a KTX2 texture with uncompressed images.
*
* The images are encoded to ETC1S block-compressed format and supercompressed
* with Basis Universal. The encoded images replace the original images and the
* texture's fields including the DFD are modified to reflect the new state.
*
* Such textures must be transcoded to a desired target block compressed format
* before they can be uploaded to a GPU via a graphics API.
*
* @sa ktxTexture2_CompressBasisEx().
*
* @param[in] This pointer to the ktxTexture2 object of interest.
* @param[in] quality Compression quality, a value from 1 - 255. Default is
128 which is selected if @p quality is 0. Lower=better
compression/lower quality/faster. Higher=less
compression/higher quality/slower.
*
* @return KTX_SUCCESS on success, other KTX_* enum values on error.
*
* @exception KTX_INVALID_OPERATION
* The texture is already supercompressed.
* @exception KTX_INVALID_OPERATION
* The texture's image are in a block compressed
* format.
* @exception KTX_OUT_OF_MEMORY Not enough memory to carry out supercompression.
*/
extern "C" KTX_error_code
ktxTexture2_CompressBasis(ktxTexture2* This, ktx_uint32_t quality)
{
ktxBasisParams params = {};
params.structSize = sizeof(params);
params.threadCount = 1;
params.compressionLevel = KTX_ETC1S_DEFAULT_COMPRESSION_LEVEL;
params.qualityLevel = quality;
return ktxTexture2_CompressBasisEx(This, &params);
}