diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 7d006c63..b974f19d 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -10,7 +10,7 @@ add_library( filament_interop SHARED src/main/cpp/filament_android.cpp - src/main/cpp/KtxReader1.cpp + ../ios/src/ktxreader/Ktx1Reader.cpp src/main/cpp/StbProvider.cpp src/main/cpp/JobSystem.cpp ../ios/src/SceneAssetLoader.cpp diff --git a/example/assets/background.ktx b/example/assets/background.ktx new file mode 100644 index 00000000..4f13ac3a --- /dev/null +++ b/example/assets/background.ktx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620bfa644724e9914df19949346ad0f441357fbb176879421d66b4377705836b +size 1048644 diff --git a/example/assets/background.png b/example/assets/background.png index cb976eb1..2ca89ebb 100644 --- a/example/assets/background.png +++ b/example/assets/background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c38fa36cbbbe969a2ae015fe56752343e8d06007c6eea28c56e22ffd56a8db1a -size 2570831 +oid sha256:bedb625eca04a9abba66d1521efda591819212e44cef0ab2438631596902140c +size 585493 diff --git a/ios/src/FilamentViewer.cpp b/ios/src/FilamentViewer.cpp index 92ed73cf..66ab31b9 100644 --- a/ios/src/FilamentViewer.cpp +++ b/ios/src/FilamentViewer.cpp @@ -59,8 +59,10 @@ #include #include +#include #include +#include #include @@ -323,74 +325,132 @@ void FilamentViewer::createImageRenderable() { _imageEntity = &imageEntity; - Texture *texture = Texture::Builder() - .width(1) - .height(1) - .levels(1) - .format(Texture::InternalFormat::RGBA8) - .sampler(Texture::Sampler::SAMPLER_2D) - .build(*_engine); - static uint32_t pixel = 0; - Texture::PixelBufferDescriptor buffer(&pixel, 4, Texture::Format::RGBA, - Texture::Type::UBYTE); - texture->setImage(*_engine, 0, std::move(buffer)); } -void FilamentViewer::setBackgroundImage(const char *resourcePath) { +static bool endsWith(string path, string ending) { + return path.compare(path.length() - ending.length(), ending.length(), ending) == 0; +} - createImageRenderable(); +void FilamentViewer::loadKtx2Texture(string path, ResourceBuffer rb) { - if (_imageTexture) { - _engine->destroy(_imageTexture); - _imageTexture = nullptr; - } + // TODO - check all this - ResourceBuffer bg = _loadResource(resourcePath); + // ktxreader::Ktx2Reader reader(*_engine); - polyvox::StreamBufferAdapter sb((char *)bg.data, (char *)bg.data + bg.size); + // reader.requestFormat(Texture::InternalFormat::DXT3_SRGBA); + // reader.requestFormat(Texture::InternalFormat::DXT3_RGBA); - std::istream *inputStream = new std::istream(&sb); + // // Uncompressed formats are lower priority, so they get added last. + // reader.requestFormat(Texture::InternalFormat::SRGB8_A8); + // reader.requestFormat(Texture::InternalFormat::RGBA8); - LinearImage *image = new LinearImage(ImageDecoder::decode( - *inputStream, resourcePath, ImageDecoder::ColorSpace::SRGB)); + // // std::ifstream inputStream("/data/data/app.polyvox.filament_example/foo.ktx", ios::binary); + + // // auto contents = vector((istreambuf_iterator(inputStream)), {}); + + // _imageTexture = reader.load(contents.data(), contents.size(), + // ktxreader::Ktx2Reader::TransferFunction::LINEAR); +} + +void FilamentViewer::loadKtxTexture(string path, ResourceBuffer rb) { + ktxreader::Ktx1Bundle *bundle = + new ktxreader::Ktx1Bundle(static_cast(rb.data), + static_cast(rb.size)); + _imageTexture = + ktxreader::Ktx1Reader::createTexture(_engine, *bundle, false, [](void* userdata) { + Ktx1Bundle* bundle = (Ktx1Bundle*) userdata; + delete bundle; + }, bundle); + + auto info = bundle->getInfo(); + _imageWidth = info.pixelWidth; + _imageHeight = info.pixelHeight; +} + +void FilamentViewer::loadPngTexture(string path, ResourceBuffer rb) { + + polyvox::StreamBufferAdapter sb((char *)rb.data, (char *)rb.data + rb.size); + + std::istream inputStream(&sb); + + LinearImage* image = new LinearImage(ImageDecoder::decode( + inputStream, path.c_str(), ImageDecoder::ColorSpace::SRGB)); if (!image->isValid()) { - Log("Invalid image : %s", resourcePath); + Log("Invalid image : %s", path.c_str()); return; } - delete inputStream; - - _freeResource(bg.id); - uint32_t channels = image->getChannels(); _imageWidth = image->getWidth(); _imageHeight = image->getHeight(); _imageTexture = Texture::Builder() - .width(_imageWidth) - .height(_imageHeight) - .levels(0xff) - .format(channels == 3 ? Texture::InternalFormat::RGB16F - : Texture::InternalFormat::RGBA16F) - .sampler(Texture::Sampler::SAMPLER_2D) - .build(*_engine); + .width(_imageWidth) + .height(_imageHeight) + .levels(0xff) + .format(channels == 3 ? Texture::InternalFormat::RGB16F + : Texture::InternalFormat::RGBA16F) + .sampler(Texture::Sampler::SAMPLER_2D) + .build(*_engine); Texture::PixelBufferDescriptor::Callback freeCallback = [](void *buf, size_t, - void *data) { - delete reinterpret_cast(data); + void *data) { + Log("Deleting LinearImage"); + delete reinterpret_cast(data); }; - Texture::PixelBufferDescriptor buffer( - image->getPixelRef(), size_t(_imageWidth * _imageHeight * channels * sizeof(float)), - channels == 3 ? Texture::Format::RGB : Texture::Format::RGBA, - Texture::Type::FLOAT, freeCallback); + auto pbd = Texture::PixelBufferDescriptor( + image->getPixelRef(), size_t(_imageWidth * _imageHeight * channels * sizeof(float)), + channels == 3 ? Texture::Format::RGB : Texture::Format::RGBA, + Texture::Type::FLOAT, nullptr, freeCallback, image); - _imageTexture->setImage(*_engine, 0, std::move(buffer)); - _imageTexture->generateMipmaps(*_engine); + _imageTexture->setImage(*_engine, 0, std::move(pbd)); +} - // This currently just acnhors the image at the bottom left of the viewport at its original size - // TODO - implement stretch/etc +void FilamentViewer::loadTextureFromPath(string path) { + string ktxExt(".ktx"); + string ktx2Ext(".ktx2"); + string pngExt(".png"); + + if (path.length() < 5) { + Log("Invalid resource path : %s", path.c_str()); + return; + } + + ResourceBuffer rb = _loadResource(path.c_str()); + + if(endsWith(path, ktxExt)) { + loadKtxTexture(path, rb); + } else if(endsWith(path, ktx2Ext)) { + loadKtx2Texture(path, rb); + } else if(endsWith(path, pngExt)) { + loadPngTexture(path, rb); + } + + _freeResource(rb.id); + +} + +void FilamentViewer::setBackgroundImage(const char *resourcePath) { + + string resourcePathString(resourcePath); + + Log("Setting background image to %s", resourcePath); + + createImageRenderable(); + + if (_imageTexture) { + Log("Destroying existing texture"); + _engine->destroy(_imageTexture); + Log("Destroyed."); + _imageTexture = nullptr; + } + + loadTextureFromPath(resourcePathString); + + // This currently just anchors the image at the bottom left of the viewport at its original size + // TODO - implement stretch/etc const Viewport& vp = _view->getViewport(); Log("Image width %d height %d vp width %d height %d", _imageWidth, _imageHeight, vp.width, vp.height); _imageScale = mat4f { float(vp.width) / float(_imageWidth) , 0.0f, 0.0f, 0.0f, 0.0f, float(vp.height) / float(_imageHeight), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; @@ -671,16 +731,18 @@ bool FilamentViewer::setCamera(SceneAsset *asset, const char *cameraName) { } void FilamentViewer::loadSkybox(const char *const skyboxPath) { - if (!skyboxPath) { - _scene->setSkybox(nullptr); - } else { + removeSkybox(); + if (skyboxPath) { ResourceBuffer skyboxBuffer = _loadResource(skyboxPath); image::Ktx1Bundle *skyboxBundle = new image::Ktx1Bundle(static_cast(skyboxBuffer.data), static_cast(skyboxBuffer.size)); _skyboxTexture = - ktxreader::Ktx1Reader::createTexture(_engine, skyboxBundle, false); + ktxreader::Ktx1Reader::createTexture(_engine, *skyboxBundle, false, [](void* userdata) { + image::Ktx1Bundle* bundle = (image::Ktx1Bundle*) userdata; + delete bundle; + }, skyboxBundle); _skybox = filament::Skybox::Builder().environment(_skyboxTexture).build(*_engine); @@ -689,15 +751,29 @@ void FilamentViewer::loadSkybox(const char *const skyboxPath) { } } -void FilamentViewer::removeSkybox() { _scene->setSkybox(nullptr); } +void FilamentViewer::removeSkybox() { + if(_skybox) { + _engine->destroy(_skybox); + _engine->destroy(_skyboxTexture); + _skybox = nullptr; + _skyboxTexture = nullptr; + } + _scene->setSkybox(nullptr); +} -void FilamentViewer::removeIbl() { _scene->setIndirectLight(nullptr); } +void FilamentViewer::removeIbl() { + if(_indirectLight) { + _engine->destroy(_indirectLight); + _engine->destroy(_iblTexture); + _indirectLight = nullptr; + _iblTexture = nullptr; + } + _scene->setIndirectLight(nullptr); +} void FilamentViewer::loadIbl(const char *const iblPath) { - if (!iblPath) { - _scene->setIndirectLight(nullptr); - } else { - + removeIbl(); + if (iblPath) { Log("Loading IBL from %s", iblPath); // Load IBL. @@ -709,7 +785,10 @@ void FilamentViewer::loadIbl(const char *const iblPath) { math::float3 harmonics[9]; iblBundle->getSphericalHarmonics(harmonics); _iblTexture = - ktxreader::Ktx1Reader::createTexture(_engine, iblBundle, false); + ktxreader::Ktx1Reader::createTexture(_engine, *iblBundle, false, [](void* userdata) { + image::Ktx1Bundle* bundle = (image::Ktx1Bundle*) userdata; + delete bundle; + }, iblBundle); _indirectLight = IndirectLight::Builder() .reflections(_iblTexture) .irradiance(3, harmonics) diff --git a/ios/src/FilamentViewer.hpp b/ios/src/FilamentViewer.hpp index 83dfb645..99ec2c1b 100644 --- a/ios/src/FilamentViewer.hpp +++ b/ios/src/FilamentViewer.hpp @@ -151,6 +151,10 @@ namespace polyvox { Material* _imageMaterial = nullptr; TextureSampler _imageSampler; ColorGrading *colorGrading = nullptr; + void loadKtx2Texture(string path, ResourceBuffer data); + void loadKtxTexture(string path, ResourceBuffer data); + void loadPngTexture(string path, ResourceBuffer data); + void loadTextureFromPath(string path); void _createManipulator(); uint32_t _lastFrameTimeInNanos; diff --git a/android/src/main/cpp/KtxReader1.cpp b/ios/src/ktxreader/Ktx1Reader.cpp similarity index 100% rename from android/src/main/cpp/KtxReader1.cpp rename to ios/src/ktxreader/Ktx1Reader.cpp diff --git a/ios/src/ktxreader/Ktx2Reader.cpp b/ios/src/ktxreader/Ktx2Reader.cpp new file mode 100644 index 00000000..622e0235 --- /dev/null +++ b/ios/src/ktxreader/Ktx2Reader.cpp @@ -0,0 +1,426 @@ +/* + * 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 + +#include +#include + +#include + +#include +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warray-bounds" +#include +#pragma clang diagnostic pop + +using namespace basist; +using namespace filament; + +using TransferFunction = ktxreader::Ktx2Reader::TransferFunction; +using Result = ktxreader::Ktx2Reader::Result; +using Async = ktxreader::Ktx2Reader::Async; +using Buffer = std::vector; + +namespace { +struct FinalFormatInfo { + const char* name; // <-- for debug purposes only + bool isSupported; + bool isCompressed; + TransferFunction transferFunction; + transcoder_texture_format basisFormat; + Texture::CompressedType compressedPixelDataType; + Texture::Type pixelDataType; + Texture::Format pixelDataFormat; +}; +} + +// This function returns various information about a Filament internal format, most notably its +// equivalent BasisU enumerant. +// +// Return by value isn't expensive here due to copy elision. +// +// Note that Filament's internal format list mimics the Vulkan format list, which +// embeds transfer function information (i.e. sRGB or not) into the format, whereas +// the basis format list does not. +// +// The following formats supported by BasisU but are not supported by Filament. +// +// transcoder_texture_format::cTFETC1_RGB +// transcoder_texture_format::cTFATC_RGB +// transcoder_texture_format::cTFATC_RGBA +// transcoder_texture_format::cTFFXT1_RGB +// transcoder_texture_format::cTFPVRTC2_4_RGB +// transcoder_texture_format::cTFPVRTC2_4_RGBA +// transcoder_texture_format::cTFPVRTC1_4_RGB +// transcoder_texture_format::cTFPVRTC1_4_RGBA +// transcoder_texture_format::cTFBC4_R +// transcoder_texture_format::cTFBC5_RG +// transcoder_texture_format::cTFBC7_RGBA (this format would add size bloat to the transcoder) +// transcoder_texture_format::cTFBGR565 (note the blue/red swap) +// +static FinalFormatInfo getFinalFormatInfo(Texture::InternalFormat fmt) { + using tif = Texture::InternalFormat; + using tct = Texture::CompressedType; + using tt = Texture::Type; + using tf = Texture::Format; + using ttf = transcoder_texture_format; + const auto sRGB = TransferFunction::sRGB; + const auto LINEAR = TransferFunction::LINEAR; + switch (fmt) { + case tif::ETC2_EAC_SRGBA8: return {"ETC2_EAC_SRGBA8", true, true, sRGB, ttf::cTFETC2_RGBA, tct::ETC2_EAC_RGBA8}; + case tif::ETC2_EAC_RGBA8: return {"ETC2_EAC_RGBA8", true, true, LINEAR, ttf::cTFETC2_RGBA, tct::ETC2_EAC_SRGBA8}; + case tif::DXT1_SRGB: return {"DXT1_SRGB", true, true, sRGB, ttf::cTFBC1_RGB, tct::DXT1_RGB}; + case tif::DXT1_RGB: return {"DXT1_RGB", true, true, LINEAR, ttf::cTFBC1_RGB, tct::DXT1_SRGB}; + case tif::DXT5_SRGBA: return {"DXT5_SRGBA", true, true, sRGB, ttf::cTFBC3_RGBA, tct::DXT5_RGBA}; + case tif::DXT5_RGBA: return {"DXT5_RGBA", true, true, LINEAR, ttf::cTFBC3_RGBA, tct::DXT5_SRGBA}; + case tif::SRGB8_ALPHA8_ASTC_4x4: return {"SRGB8_ALPHA8_ASTC_4x4", true, true, sRGB, ttf::cTFASTC_4x4_RGBA, tct::RGBA_ASTC_4x4}; + case tif::RGBA_ASTC_4x4: return {"RGBA_ASTC_4x4", true, true, LINEAR, ttf::cTFASTC_4x4_RGBA, tct::SRGB8_ALPHA8_ASTC_4x4}; + case tif::EAC_R11: return {"EAC_R11", true, true, LINEAR, ttf::cTFETC2_EAC_R11, tct::EAC_R11}; + + // The following format is useful for normal maps. + // Note that BasisU supports only the unsigned variant. + case tif::EAC_RG11: return {"EAC_RG11", true, true, LINEAR, ttf::cTFETC2_EAC_RG11, tct::EAC_RG11}; + + // Uncompressed formats. + case tif::SRGB8_A8: return {"SRGB8_A8", true, false, sRGB, ttf::cTFRGBA32, {}, tt::UBYTE, tf::RGBA}; + case tif::RGBA8: return {"RGBA8", true, false, LINEAR, ttf::cTFRGBA32, {}, tt::UBYTE, tf::RGBA}; + case tif::RGB565: return {"RGB565", true, false, LINEAR, ttf::cTFRGB565, {}, tt::USHORT_565, tf::RGB}; + case tif::RGBA4: return {"RGBA4", true, false, LINEAR, ttf::cTFRGBA4444, {}, tt::USHORT, tf::RGBA}; + + default: return {}; + } +} + +// In theory we could pass "free" directly into the callback but doing so triggers ASAN warnings. +static void freeCallback(void* buf, size_t, void* userdata) { + free(buf); +} + +// This helper is used by both the asynchronous and synchronous API's. +static Result transcodeImageLevel(ktx2_transcoder& transcoder, + ktx2_transcoder_state& transcoderState, Texture::InternalFormat format, + uint32_t levelIndex, Texture::PixelBufferDescriptor** pbd) { + using basisu::texture_format; + assert_invariant(levelIndex < KTX2_MAX_SUPPORTED_LEVEL_COUNT); + const FinalFormatInfo formatInfo = getFinalFormatInfo(format); + const texture_format destFormat = basis_get_basisu_texture_format(formatInfo.basisFormat); + const uint32_t layerIndex = 0; + const uint32_t faceIndex = 0; + const uint32_t decodeFlags = 0; + const uint32_t outputRowPitch = 0; + const uint32_t outputRowCount = 0; + const int channel0 = 0; + const int channel1 = 0; + + basist::ktx2_image_level_info levelInfo; + transcoder.get_image_level_info(levelInfo, levelIndex, layerIndex, faceIndex); + + if (formatInfo.isCompressed) { + const uint32_t qwordsPerBlock = basisu::get_qwords_per_block(destFormat); + const size_t byteCount = sizeof(uint64_t) * qwordsPerBlock * levelInfo.m_total_blocks; + uint64_t* const blocks = (uint64_t*) malloc(byteCount); + if (!transcoder.transcode_image_level(levelIndex, layerIndex, faceIndex, blocks, + levelInfo.m_total_blocks, formatInfo.basisFormat, decodeFlags, + outputRowPitch, outputRowCount, channel0, + channel1, &transcoderState)) { + return Result::COMPRESSED_TRANSCODE_FAILURE; + } + *pbd = new Texture::PixelBufferDescriptor(blocks, + byteCount, formatInfo.compressedPixelDataType, byteCount, freeCallback); + return Result::SUCCESS; + } + + const uint32_t rowCount = levelInfo.m_orig_height; + const uint32_t bytesPerPix = basis_get_bytes_per_block_or_pixel(formatInfo.basisFormat); + const size_t byteCount = bytesPerPix * levelInfo.m_orig_width * rowCount; + uint64_t* const rows = (uint64_t*) malloc(byteCount); + if (!transcoder.transcode_image_level(levelIndex, layerIndex, faceIndex, rows, + byteCount / bytesPerPix, formatInfo.basisFormat, decodeFlags, + outputRowPitch, outputRowCount, channel0, channel1, &transcoderState)) { + return Result::UNCOMPRESSED_TRANSCODE_FAILURE; + } + *pbd = new Texture::PixelBufferDescriptor(rows, byteCount, + formatInfo.pixelDataFormat, formatInfo.pixelDataType, freeCallback); + return Result::SUCCESS; +} + +namespace ktxreader { + +class FAsync : public Async { +public: + FAsync(Texture* texture, Engine& engine, ktx2_transcoder* transcoder, Buffer&& buf) : + mTexture(texture), mEngine(engine), mTranscoder(transcoder), + mSourceBuffer(std::move(buf)) {} + Texture* getTexture() const noexcept { return mTexture; } + Result doTranscoding(); + void uploadImages(); + +private: + using TranscoderResult = std::atomic; + + // After each level is transcoded, the results are stashed in the following array until the + // foreground thread calls uploadImages(). Each slot in the array corresponds to a single + // miplevel in the texture. + TranscoderResult mTranscoderResults[KTX2_MAX_SUPPORTED_LEVEL_COUNT] = {}; + + Texture* const mTexture; + Engine& mEngine; + + // We do not share the BasisU trancoder between Async objects. The BasisU transcoder + // allows parallelization at "level" granularity, but does not permit parallelization at + // "texture" granularity. i.e. the transcode_image_level() method is thread-safe but the + // start_transcoding() method is not. + std::unique_ptr const mTranscoder; + + // Storage for the content of the KTX2 file. + Buffer mSourceBuffer; +}; + +Ktx2Reader::Ktx2Reader(Engine& engine, bool quiet) : + mEngine(engine), + mQuiet(quiet), + mTranscoder(new ktx2_transcoder()) { + mRequestedFormats.reserve((size_t) transcoder_texture_format::cTFTotalTextureFormats); + basisu_transcoder_init(); +} + +Ktx2Reader::~Ktx2Reader() { + delete mTranscoder; +} + +Result Ktx2Reader::requestFormat(Texture::InternalFormat format) noexcept { + if (!getFinalFormatInfo(format).isSupported) { + return Result::FORMAT_UNSUPPORTED; + } + for (Texture::InternalFormat fmt : mRequestedFormats) { + if (fmt == format) { + return Result::FORMAT_ALREADY_REQUESTED; + } + } + mRequestedFormats.push_back(format); + return Result::SUCCESS; +} + +void Ktx2Reader::unrequestFormat(Texture::InternalFormat format) noexcept { + for (auto iter = mRequestedFormats.begin(); iter != mRequestedFormats.end(); ++iter) { + if (*iter == format) { + mRequestedFormats.erase(iter); + return; + } + } +} + +Texture* Ktx2Reader::load(const void* data, size_t size, TransferFunction transfer) { + Texture* texture = createTexture(mTranscoder, data, size, transfer); + if (texture == nullptr) { + return nullptr; + } + + if (!mTranscoder->start_transcoding()) { + mEngine.destroy(texture); + if (!mQuiet) { + utils::slog.e << "BasisU start_transcoding failed." << utils::io::endl; + } + return nullptr; + } + + ktx2_transcoder_state basisThreadState; + basisThreadState.clear(); + + for (uint32_t levelIndex = 0, n = mTranscoder->get_levels(); levelIndex < n; levelIndex++) { + Texture::PixelBufferDescriptor* pbd; + Result result = transcodeImageLevel(*mTranscoder, basisThreadState, texture->getFormat(), + levelIndex, &pbd); + if (UTILS_UNLIKELY(result != Result::SUCCESS)) { + mEngine.destroy(texture); + if (!mQuiet) { + utils::slog.e << "Failed to transcode level " << levelIndex << utils::io::endl; + } + return nullptr; + } + texture->setImage(mEngine, levelIndex, std::move(*pbd)); + } + return texture; +} + +Result FAsync::doTranscoding() { + ktx2_transcoder_state basisThreadState; + basisThreadState.clear(); + for (uint32_t levelIndex = 0, n = mTranscoder->get_levels(); levelIndex < n; levelIndex++) { + Texture::PixelBufferDescriptor* pbd; + Result result = transcodeImageLevel(*mTranscoder, basisThreadState, mTexture->getFormat(), + levelIndex, &pbd); + if (UTILS_UNLIKELY(result != Result::SUCCESS)) { + return result; + } + mTranscoderResults[levelIndex].store(pbd); + } + return Result::SUCCESS; +} + +void FAsync::uploadImages() { + size_t levelIndex = 0; + UTILS_NOUNROLL + for (TranscoderResult& level : mTranscoderResults) { + Texture::PixelBufferDescriptor* pbd = level.load(); + if (pbd) { + level.store(nullptr); + mTexture->setImage(mEngine, levelIndex, std::move(*pbd)); + delete pbd; + } + ++levelIndex; + } +} + +Async* Ktx2Reader::asyncCreate(const void* data, size_t size, TransferFunction transfer) { + Buffer ktx2content((uint8_t*)data, (uint8_t*)data + size); + ktx2_transcoder* transcoder = new ktx2_transcoder(); + Texture* texture = createTexture(transcoder, ktx2content.data(), ktx2content.size(), transfer); + if (texture == nullptr) { + delete transcoder; + return nullptr; + } + if (!transcoder->start_transcoding()) { + delete transcoder; + mEngine.destroy(texture); + return nullptr; + } + // There's no need to do any further work at this point but it should be noted that this is the + // point at which we first come to know the number of miplevels, dimensions, etc. If we had a + // dynamically sized array to store decoder results, we would reserve it here. + return new FAsync(texture, mEngine, transcoder, std::move(ktx2content)); +} + +void Ktx2Reader::asyncDestroy(Async** async) { + delete *async; + *async = nullptr; +} + +Texture* Ktx2Reader::createTexture(ktx2_transcoder* transcoder, const void* data, size_t size, + TransferFunction transfer) { + if (!transcoder->init(data, size)) { + if (!mQuiet) { + utils::slog.e << "BasisU transcoder init failed." << utils::io::endl; + } + return nullptr; + } + + if (transcoder->get_dfd_transfer_func() == KTX2_KHR_DF_TRANSFER_LINEAR && + transfer == TransferFunction::sRGB) { + if (!mQuiet) { + utils::slog.e << "Source texture is marked linear, but client is requesting sRGB." + << utils::io::endl; + } + return nullptr; + } + + if (transcoder->get_dfd_transfer_func() == KTX2_KHR_DF_TRANSFER_SRGB && + transfer == TransferFunction::LINEAR) { + if (!mQuiet) { + utils::slog.e << "Source texture is marked sRGB, but client is requesting linear." + << utils::io::endl; + } + return nullptr; + } + + // TODO: support cubemaps. For now we use KTX1 for cubemaps because basisu does not support HDR. + if (transcoder->get_faces() == 6) { + if (!mQuiet) { + utils::slog.e << "Cubemaps are not yet supported." << utils::io::endl; + } + return nullptr; + } + + // TODO: support texture arrays. + if (transcoder->get_layers() > 1) { + if (!mQuiet) { + utils::slog.e << "Texture arrays are not yet supported." << utils::io::endl; + } + return nullptr; + } + + // First pass through, just to make sure we can transcode it. + bool found = false; + Texture::InternalFormat resolvedFormat; + FinalFormatInfo info; + for (Texture::InternalFormat requestedFormat : mRequestedFormats) { + if (!Texture::isTextureFormatSupported(mEngine, requestedFormat)) { + continue; + } + info = getFinalFormatInfo(requestedFormat); + if (!info.isSupported || info.transferFunction != transfer) { + continue; + } + if (!basis_is_format_supported(info.basisFormat, transcoder->get_format())) { + continue; + } + const uint32_t layerIndex = 0; + const uint32_t faceIndex = 0; + for (uint32_t levelIndex = 0; levelIndex < transcoder->get_levels(); levelIndex++) { + basist::ktx2_image_level_info info; + if (!transcoder->get_image_level_info(info, levelIndex, layerIndex, faceIndex)) { + continue; + } + } + found = true; + resolvedFormat = requestedFormat; + break; + } + + if (!found) { + if (!mQuiet) { + utils::slog.e << "Unable to decode any of the requested formats." << utils::io::endl; + } + return nullptr; + } + + Texture* texture = Texture::Builder() + .width(transcoder->get_width()) + .height(transcoder->get_height()) + .levels(transcoder->get_levels()) + .sampler(Texture::Sampler::SAMPLER_2D) + .format(resolvedFormat) + .build(mEngine); + + if (texture == nullptr && !mQuiet) { + utils::slog.e << "Unable to construct texture using BasisU info." << utils::io::endl; + } + + #if BASISU_FORCE_DEVEL_MESSAGES + utils::slog.e << "Ktx2Reader created " + << transcoder->get_width() << "x" << transcoder->get_height() << " texture with format " + << info.name << utils::io::endl; + #endif + + return texture; +} + +Texture* Async::getTexture() const noexcept { + return static_cast(this)->getTexture(); +} + +Result Async::doTranscoding() { + return static_cast(this)->doTranscoding(); +} + +void Async::uploadImages() { + return static_cast(this)->uploadImages(); +} + +} // namespace ktxreader