Add Ktx2Reader and BasisEncoder and use them in samples.

mipgen can now emit basis-encoded KTX2 files. Both the desktop and
web "suzanne" samples use this as a test for compressed textures.

This PR does not add KTX2 support to glTF, but it's on the way.

`BasisEncoder` has a builder style API that calls the basis encoder to
create KTX2 files. This hides some low-level BasisU features that we are
not using, like file I/O and mipmap generation.

`Ktx2Reader` is an easy-to-use API for creating Filament textures from
KTX2 files. Its API primarily consists of these two methods:

    bool requestFormat(Texture::InternalFormat format);
    Filament::Texture* load(const uint8_t* data, size_t size);

The first method is used to build an ordered list of formats that are
supported by your hardware. The second method consumes the contents of a
basis-encoded KTX2 file and attempts to produce a Filament texture with
a preferred format.

IMPORTANT: Our tools still let you use KTX1 for non-compressed images
because it is useful for HDR, but you can no longer use KTX1 for
block-compressed data.

Partial fix for #4771.
This commit is contained in:
Philip Rideout
2022-03-31 13:26:02 -07:00
parent 550fcfe2e3
commit 9aeec3a759
30 changed files with 1064 additions and 363 deletions

View File

@@ -0,0 +1,267 @@
/*
* Copyright (C) 2022 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 <imageio/BasisEncoder.h>
#include <image/ColorTransform.h>
#include <image/ImageOps.h>
#include <utils/debug.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warray-bounds"
#include <basisu_comp.h>
#pragma clang diagnostic pop
namespace image {
using Builder = BasisEncoder::Builder;
struct BasisEncoderBuilderImpl {
basisu::basis_compressor_params params = {};
bool grayscale = false;
bool linear = false;
bool normals = false;
bool quiet = false;
size_t jobs = 4;
bool error = false;
};
struct BasisEncoderImpl {
basisu::basis_compressor* encoder;
basisu::job_pool* jobs;
bool quiet;
};
Builder::Builder(size_t mipCount, size_t layerCount) noexcept : mImpl(new BasisEncoderBuilderImpl) {
const size_t mipsPerImage = mipCount > 0 ? mipCount : 1;
const bool multiple = mImpl->params.m_source_images.size() > 1;
mImpl->params.m_tex_type = multiple ? basist::cBASISTexType2DArray : basist::cBASISTexType2D;
mImpl->params.m_uastc = true;
mImpl->params.m_source_images.resize(layerCount);
mImpl->params.m_source_mipmap_images.resize(layerCount);
if (mipCount > 1) {
for (size_t layer = 0; layer < layerCount; ++layer) {
mImpl->params.m_source_mipmap_images[layer].resize(mipCount - 1);
}
}
}
Builder::~Builder() noexcept { delete mImpl; }
Builder::Builder(Builder&& that) noexcept {
std::swap(mImpl, that.mImpl);
}
Builder& Builder::operator=(Builder&& that) noexcept {
std::swap(mImpl, that.mImpl);
return *this;
}
Builder& Builder::miplevel(size_t level, size_t layer, const LinearImage& floatImage) noexcept {
if (layer >= mImpl->params.m_source_images.size()) {
assert_invariant(false);
mImpl->error = true;
return *this;
}
auto& basisBaseLevel = mImpl->params.m_source_images[layer];
auto& basisMipmaps = mImpl->params.m_source_mipmap_images[layer];
if (level >= basisMipmaps.size() + 1) {
assert_invariant(false);
mImpl->error = true;
return *this;
}
basisu::image* basisImage = level == 0 ? &basisBaseLevel : &basisMipmaps[level - 1];
LinearImage sourceImage = mImpl->normals ? vectorsToColors(floatImage) : floatImage;
const bool applyTransferFunction = !mImpl->linear;
if (mImpl->grayscale) {
std::unique_ptr<uint8_t[]> data = applyTransferFunction ?
fromLinearTosRGB<uint8_t, 1>(sourceImage) : fromLinearToGrayscale<uint8_t>(sourceImage);
basisImage->init(data.get(), floatImage.getWidth(), floatImage.getHeight(), 1);
} else {
std::unique_ptr<uint8_t[]> data = applyTransferFunction ?
fromLinearTosRGB<uint8_t, 4>(sourceImage) : fromLinearToRGB<uint8_t, 4>(sourceImage);
basisImage->init(data.get(), floatImage.getWidth(), floatImage.getHeight(), 4);
}
return *this;
}
Builder& Builder::linear(bool enabled) noexcept {
mImpl->linear = enabled;
return *this;
}
Builder& Builder::cubemap(bool enabled) noexcept {
if (enabled) {
mImpl->params.m_tex_type = basist::cBASISTexTypeCubemapArray;
} else {
const bool multi = mImpl->params.m_source_images.size() > 1;
mImpl->params.m_tex_type = multi ? basist::cBASISTexType2DArray : basist::cBASISTexType2D;
}
return *this;
}
Builder& Builder::intermediateFormat(BasisEncoder::IntermediateFormat format) noexcept {
mImpl->params.m_uastc = format == IntermediateFormat::UASTC;
return *this;
}
Builder& Builder::grayscale(bool enabled) noexcept {
mImpl->grayscale = enabled;
return *this;
}
Builder& Builder::normals(bool enabled) noexcept {
mImpl->normals = enabled;
return *this;
}
Builder& Builder::jobs(size_t count) noexcept {
mImpl->jobs = count;
return *this;
}
Builder& Builder::quiet(bool enabled) noexcept {
mImpl->quiet = enabled;
return *this;
}
BasisEncoder* Builder::build() {
if (mImpl->error) {
return nullptr;
}
basisu::basisu_encoder_init();
auto& params = mImpl->params;
params.m_status_output = !mImpl->quiet;
params.m_pJob_pool = new basisu::job_pool(mImpl->jobs);
params.m_create_ktx2_file = true;
params.m_ktx2_uastc_supercompression = basist::KTX2_SS_ZSTANDARD;
// This sRGB flag doesn't actually affect the compression scheme or the basis format, it's just
// an annotation that gets stored in the KTX2 file, which enables the app to choose the right
// format when it loads and transcodes the texture. Technically however, the transcoder
// SHOULD know about this, since in some scenarios it needs to interpolate between colors.
params.m_ktx2_srgb_transfer_func = !mImpl->linear;
// Select the same quality that the basis tool selects by default (midpoint of range).
params.m_quality_level = 128;
// This is the default zstd compression level used by the basisu cmdline cool.
params.m_ktx2_zstd_supercompression_level = 6;
// We do not want basis to read from files, we want it to read from "m_source_images"
params.m_read_source_images = false;
// We do not want basis to write the file, we want to manually dump "get_output_ktx2_file()"
params.m_write_output_basis_files = false;
basisu::basis_compressor* encoder = new basisu::basis_compressor();
if (!encoder->init(params)) {
assert_invariant(false);
delete encoder;
return nullptr;
}
return new BasisEncoder(new BasisEncoderImpl {
.encoder = encoder,
.jobs = params.m_pJob_pool,
.quiet = mImpl->quiet,
});
}
BasisEncoder::BasisEncoder(BasisEncoderImpl* impl) noexcept : mImpl(impl) {}
BasisEncoder::~BasisEncoder() noexcept {
delete mImpl->encoder;
delete mImpl->jobs;
delete mImpl;
basisu::basisu_encoder_deinit();
}
BasisEncoder::BasisEncoder(BasisEncoder&& that) noexcept {
std::swap(mImpl, that.mImpl);
}
BasisEncoder& BasisEncoder::operator=(BasisEncoder&& that) noexcept {
std::swap(mImpl, that.mImpl);
return *this;
}
bool BasisEncoder::encode() {
using namespace basisu;
basis_compressor::error_code ec = mImpl->encoder->process();
switch (ec)
{
case basis_compressor::cECSuccess:
if (!mImpl->quiet) {
puts("Compression succeeded.");
}
return true;
case basis_compressor::cECFailedReadingSourceImages:
puts("Compressor failed reading a source image!");
break;
case basis_compressor::cECFailedValidating:
puts("Compressor failed 2darray/cubemap/video validation checks!");
break;
case basis_compressor::cECFailedEncodeUASTC:
puts("Compressor UASTC encode failed!");
break;
case basis_compressor::cECFailedFrontEnd:
puts("Compressor frontend stage failed!");
break;
case basis_compressor::cECFailedFontendExtract:
puts("Compressor frontend data extraction failed!");
break;
case basis_compressor::cECFailedBackend:
puts("Compressor backend stage failed!");
break;
case basis_compressor::cECFailedCreateBasisFile:
puts("Compressor failed creating Basis file data!");
break;
case basis_compressor::cECFailedWritingOutput:
puts("Compressor failed writing to output Basis file!");
break;
case basis_compressor::cECFailedUASTCRDOPostProcess:
puts("Compressor failed during the UASTC post process step!");
break;
case basis_compressor::cECFailedCreateKTX2File:
puts("Compressor failed creating KTX2 file data!");
break;
default:
puts("basis_compress::process() failed!");
break;
}
return false;
}
size_t BasisEncoder::getKtx2ByteCount() const noexcept {
return mImpl->encoder->get_output_ktx2_file().size();
}
uint8_t const* BasisEncoder::getKtx2Data() const noexcept {
return mImpl->encoder->get_output_ktx2_file().data();
}
} // namespace image