From d76551ab2806243fb1f6021087dcd05be67fe508 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 28 Apr 2022 15:08:26 -0700 Subject: [PATCH] Add async interface to Ktx2Reader. --- android/gltfio-android/CMakeLists.txt | 13 +- .../gltf-viewer.xcodeproj/project.pbxproj | 4 + ios/samples/gltf-viewer/project.yml | 2 +- libs/gltfio/CMakeLists.txt | 2 +- libs/gltfio/src/AssetLoader.cpp | 2 +- libs/ktxreader/include/ktxreader/Ktx2Reader.h | 116 ++++++- libs/ktxreader/src/Ktx1Reader.cpp | 9 +- libs/ktxreader/src/Ktx2Reader.cpp | 325 +++++++++++++----- third_party/basisu/tnt/CMakeLists.txt | 1 + web/filament-js/extensions.js | 4 +- web/filament-js/filament.d.ts | 13 + web/filament-js/jsenums.cpp | 9 +- 12 files changed, 389 insertions(+), 111 deletions(-) diff --git a/android/gltfio-android/CMakeLists.txt b/android/gltfio-android/CMakeLists.txt index 3a5b4f5f4f..bb35e919a7 100644 --- a/android/gltfio-android/CMakeLists.txt +++ b/android/gltfio-android/CMakeLists.txt @@ -11,10 +11,18 @@ add_library(dracodec STATIC IMPORTED) set_target_properties(dracodec PROPERTIES IMPORTED_LOCATION ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libdracodec.a) +add_library(ktxreader STATIC IMPORTED) +set_target_properties(ktxreader PROPERTIES IMPORTED_LOCATION + ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libktxreader.a) + add_library(stb STATIC IMPORTED) set_target_properties(stb PROPERTIES IMPORTED_LOCATION ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libstb.a) +add_library(basis_transcoder STATIC IMPORTED) +set_target_properties(basis_transcoder PROPERTIES IMPORTED_LOCATION + ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libbasis_transcoder.a) + add_library(utils STATIC IMPORTED) set_target_properties(utils PROPERTIES IMPORTED_LOCATION ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libutils.a) @@ -86,6 +94,7 @@ set(GLTFIO_INCLUDE_DIRS ../../third_party/hat-trie ../../third_party/stb ../../libs/utils/include + ../../libs/ktxreader/include ) add_library(gltfio-jni SHARED ${GLTFIO_SRCS}) @@ -95,9 +104,9 @@ set_target_properties(gltfio-jni PROPERTIES LINK_DEPENDS ${CMAKE_CURRENT_SOURCE_ if(GLTFIO_LITE) target_compile_definitions(gltfio-jni PUBLIC GLTFIO_LITE=1) - target_link_libraries(gltfio-jni filament-jni utils log stb gltfio_resources_lite) + target_link_libraries(gltfio-jni filament-jni utils log stb ktxreader basis_transcoder gltfio_resources_lite) else() - target_link_libraries(gltfio-jni filament-jni utils log stb gltfio_resources) + target_link_libraries(gltfio-jni filament-jni utils log stb ktxreader basis_transcoder gltfio_resources) # Enable Draco in the non-lite variant of gltfio. target_link_libraries(gltfio-jni dracodec) diff --git a/ios/samples/gltf-viewer/gltf-viewer.xcodeproj/project.pbxproj b/ios/samples/gltf-viewer/gltf-viewer.xcodeproj/project.pbxproj index cffffaf578..7e030ae056 100644 --- a/ios/samples/gltf-viewer/gltf-viewer.xcodeproj/project.pbxproj +++ b/ios/samples/gltf-viewer/gltf-viewer.xcodeproj/project.pbxproj @@ -424,6 +424,7 @@ "-limage", "-lgeometry", "-lstb", + "-lbasis_transcoder", "-lcamutils", "-ldracodec", "-lviewer", @@ -475,6 +476,7 @@ "-limage", "-lgeometry", "-lstb", + "-lbasis_transcoder", "-lcamutils", "-ldracodec", "-lviewer", @@ -588,6 +590,7 @@ "-limage", "-lgeometry", "-lstb", + "-lbasis_transcoder", "-lcamutils", "-ldracodec", "-lviewer", @@ -639,6 +642,7 @@ "-limage", "-lgeometry", "-lstb", + "-lbasis_transcoder", "-lcamutils", "-ldracodec", "-lviewer", diff --git a/ios/samples/gltf-viewer/project.yml b/ios/samples/gltf-viewer/project.yml index 5d9de266d9..d7ec46d6e2 100644 --- a/ios/samples/gltf-viewer/project.yml +++ b/ios/samples/gltf-viewer/project.yml @@ -19,7 +19,7 @@ targets: - FilamentApp settings: base: - OTHER_LDFLAGS: ["-lgltfio_core", "-lgltfio_resources", "-limage", "-lgeometry", "-lstb", + OTHER_LDFLAGS: ["-lgltfio_core", "-lgltfio_resources", "-limage", "-lgeometry", "-lstb", "-lbasis_transcoder", "-lcamutils", "-ldracodec", "-lviewer", "-lcivetweb", "-lktxreader"] preBuildScripts: - path: build-resources.sh diff --git a/libs/gltfio/CMakeLists.txt b/libs/gltfio/CMakeLists.txt index 9fe414a458..cf1e45d23b 100644 --- a/libs/gltfio/CMakeLists.txt +++ b/libs/gltfio/CMakeLists.txt @@ -165,7 +165,7 @@ add_library(gltfio_resources_lite ${DUMMY_SRC} ${RESGEN_SOURCE}) # ================================================================================================== include_directories(${PUBLIC_HDR_DIR} ${RESOURCE_DIR}) -link_libraries(math utils filament cgltf stb geometry gltfio_resources tsl trie) +link_libraries(math utils filament cgltf stb ktxreader geometry gltfio_resources tsl trie) add_library(gltfio_core STATIC ${PUBLIC_HDRS} ${SRCS}) diff --git a/libs/gltfio/src/AssetLoader.cpp b/libs/gltfio/src/AssetLoader.cpp index ddbc461947..b0a9a01fad 100644 --- a/libs/gltfio/src/AssetLoader.cpp +++ b/libs/gltfio/src/AssetLoader.cpp @@ -1326,7 +1326,7 @@ void FAssetLoader::addTextureBinding(MaterialInstance* materialInstance, const c dstSampler.setWrapModeS(TextureSampler::WrapMode::REPEAT); dstSampler.setWrapModeT(TextureSampler::WrapMode::REPEAT); - // These defaults are up the implementation but since we generate mipmaps unconditionally, + // These defaults are up the implementation but since we try to provide mipmaps, // we might as well use them. In practice the conformance models look awful without // using mipmapping by default. dstSampler.setMagFilter(TextureSampler::MagFilter::LINEAR); diff --git a/libs/ktxreader/include/ktxreader/Ktx2Reader.h b/libs/ktxreader/include/ktxreader/Ktx2Reader.h index 2f2aade597..91c5a53721 100644 --- a/libs/ktxreader/include/ktxreader/Ktx2Reader.h +++ b/libs/ktxreader/include/ktxreader/Ktx2Reader.h @@ -40,6 +40,14 @@ class Ktx2Reader { using Texture = filament::Texture; enum class TransferFunction { LINEAR, sRGB }; + enum class Result { + SUCCESS, + COMPRESSED_TRANSCODE_FAILURE, + UNCOMPRESSED_TRANSCODE_FAILURE, + FORMAT_UNSUPPORTED, + FORMAT_ALREADY_REQUESTED, + }; + Ktx2Reader(Engine& engine, bool quiet = false); ~Ktx2Reader(); @@ -55,18 +63,18 @@ class Ktx2Reader { * considered higher priority. * * If BasisU knows a priori that the given format is not available (e.g. if the build has - * disabled it), this returns false and the format is not added to the list. + * disabled it), the format is not added and FORMAT_UNSUPPORTED is returned. * - * Returns false if the given format has already been requested. + * Returns FORMAT_ALREADY_REQUESTED if the given format has already been requested. * * Hint: BasisU supports the following uncompressed formats: RGBA8, RGB565, RGBA4. */ - bool requestFormat(Texture::InternalFormat format); + Result requestFormat(Texture::InternalFormat format) noexcept; /** * Removes the given format from the list, or does nothing if it hasn't been requested. */ - void unrequestFormat(Texture::InternalFormat format); + void unrequestFormat(Texture::InternalFormat format) noexcept; /** * Attempts to create and load a Filament texture from the given KTX2 blob. @@ -83,7 +91,97 @@ class Ktx2Reader { * in the KTX2 blob. If they do not match, this method fails. * 2) It is used as a filter when determining the final internal format. */ - Texture* load(const uint8_t* data, size_t size, TransferFunction transfer); + Texture* load(const void* data, size_t size, TransferFunction transfer); + + /** + * Asynchronous Interface + * ====================== + * + * Alternative API suitable for asynchronous transcoding of mipmap levels. + * If unsure that you need to use this, then don't, just call load() instead. + * Usage pseudocode: + * + * auto async = reader->asyncCreate(data, size, TransferFunction::LINEAR); + * mTexture = async->getTexture(); + * auto backgroundThread = spawnThread({ async->doTranscoding(); }) + * backgroundThread.wait(); + * async->uploadImages(); + * reader->asyncDestroy(async); + * + * In the documentation comments, "foreground thread" refers to the thread that the + * Filament Engine was created on. + */ + class Async { + public: + /** + * Retrieves the Texture object. + * + * The texture is available immediately, but does not have its miplevels ready until + * after doTranscoding() and the subsequent uploadImages() have been completed. The + * caller has ownership over this texture and is responsible for freeing it after all + * miplevels have been uploaded. + */ + Texture* getTexture() const noexcept; + + /** + * Loads all mipmaps from the KTX2 file and transcodes them to the resolved format. + * + * This does not return until all mipmaps have been transcoded. This is typically + * called from a background thread. + */ + Result doTranscoding(); + + /** + * Uploads pending mipmaps to the texture. + * + * This can safely be called while doTranscoding() is still working in another thread. + * Since this calls Texture::setImage(), it should be called from the foreground thread; + * see "Thread safety" in the documentation for filament::Engine. + */ + void uploadImages(); + + protected: + Async() noexcept = default; + ~Async() = default; + + public: + Async(Async const&) = delete; + Async(Async&&) = delete; + Async& operator=(Async const&) = delete; + Async& operator=(Async&&) = delete; + + friend class Ktx2Reader; + }; + + /** + * Creates a texture without starting the transcode process. + * + * This method is an alternative to load() that allows users to populate mipmap levels + * asynchronously. The texture object however is still created synchronously. + * + * - For a usage example, see the documentation for the Async object. + * - Creates a copy of the given buffer, allowing clients to free it immediately. + * - Returns null if none of the requested formats can be extracted from the data. + * + * This method iterates through the requested format list, checking each one against the + * platform's capabilities and its availability from the transcoder. When a suitable format + * is determined, it then performs lossless decompression (zstd) before transcoding the data + * into the final format. + * + * The transfer function specified here is used in two ways: + * 1) It is checked against the transfer function that was specified as metadata + * in the KTX2 blob. If they do not match, this method fails. + * 2) It is used as a filter when determining the final internal format. + */ + Async* asyncCreate(const void* data, size_t size, TransferFunction transfer); + + /** + * Frees the given async object and sets it to null. + * + * This frees the original source data (i.e. the raw content of the KTX2 file) but does not + * free the associated Texture object. This can be done after transcoding has finished. + */ + void asyncDestroy(Async** async); private: Ktx2Reader(const Ktx2Reader&) = delete; @@ -91,11 +189,13 @@ class Ktx2Reader { Ktx2Reader(Ktx2Reader&& that) noexcept = delete; Ktx2Reader& operator=(Ktx2Reader&& that) noexcept = delete; - Engine& mEngine; - bool mQuiet; - basist::ktx2_transcoder* const mTranscoder; + Texture* createTexture(basist::ktx2_transcoder* transcoder, const void* data, + size_t size, TransferFunction transfer); + Engine& mEngine; + basist::ktx2_transcoder* const mTranscoder; utils::FixedCapacityVector mRequestedFormats; + bool mQuiet; }; } // namespace ktxreader diff --git a/libs/ktxreader/src/Ktx1Reader.cpp b/libs/ktxreader/src/Ktx1Reader.cpp index b7f06892f0..9148d861ec 100644 --- a/libs/ktxreader/src/Ktx1Reader.cpp +++ b/libs/ktxreader/src/Ktx1Reader.cpp @@ -33,11 +33,16 @@ Texture* createTexture(Engine* engine, const Ktx1Bundle& ktx, bool srgb, const auto dataformat = toPixelDataFormat(ktxinfo); auto texformat = toTextureFormat(ktxinfo); + +#ifndef NDEBUG if (srgb && !isSrgbTextureFormat(texformat)) { - utils::slog.w << "Requested sRGB format but KTX contains a linear format. "; + utils::slog.w << "Requested sRGB format but KTX contains a linear format. " + << utils::io::endl; } else if (!srgb && isSrgbTextureFormat(texformat)) { - utils::slog.w << "Requested linear format but KTX contains a sRGB format. "; + utils::slog.w << "Requested linear format but KTX contains a sRGB format. " + << utils::io::endl; } +#endif Texture* texture = Texture::Builder() .width(ktxinfo.pixelWidth) diff --git a/libs/ktxreader/src/Ktx2Reader.cpp b/libs/ktxreader/src/Ktx2Reader.cpp index 1b47cee541..bd501837ef 100644 --- a/libs/ktxreader/src/Ktx2Reader.cpp +++ b/libs/ktxreader/src/Ktx2Reader.cpp @@ -21,6 +21,8 @@ #include +#include + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warray-bounds" #include @@ -30,9 +32,13 @@ 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; @@ -73,35 +79,118 @@ static FinalFormatInfo getFinalFormatInfo(Texture::InternalFormat fmt) { using tt = Texture::Type; using tf = Texture::Format; using ttf = transcoder_texture_format; - const auto sRGB = ktxreader::Ktx2Reader::TransferFunction::sRGB; - const auto LINEAR = ktxreader::Ktx2Reader::TransferFunction::LINEAR; + const auto sRGB = TransferFunction::sRGB; + const auto LINEAR = TransferFunction::LINEAR; switch (fmt) { - case tif::ETC2_EAC_SRGBA8: return {true, true, sRGB, ttf::cTFETC2_RGBA, tct::ETC2_EAC_RGBA8}; - case tif::ETC2_EAC_RGBA8: return {true, true, LINEAR, ttf::cTFETC2_RGBA, tct::ETC2_EAC_SRGBA8}; - case tif::DXT1_SRGB: return {true, true, sRGB, ttf::cTFBC1_RGB, tct::DXT1_RGB}; - case tif::DXT1_RGB: return {true, true, LINEAR, ttf::cTFBC1_RGB, tct::DXT1_SRGB}; - case tif::DXT3_SRGBA: return {true, true, sRGB, ttf::cTFBC3_RGBA, tct::DXT3_RGBA}; - case tif::DXT3_RGBA: return {true, true, LINEAR, ttf::cTFBC3_RGBA, tct::DXT3_SRGBA}; - case tif::SRGB8_ALPHA8_ASTC_4x4: return {true, true, sRGB, ttf::cTFASTC_4x4_RGBA, tct::RGBA_ASTC_4x4}; - case tif::RGBA_ASTC_4x4: return {true, true, LINEAR, ttf::cTFASTC_4x4_RGBA, tct::SRGB8_ALPHA8_ASTC_4x4}; - case tif::EAC_R11: return {true, true, LINEAR, ttf::cTFETC2_EAC_R11, tct::EAC_R11}; + 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::DXT3_SRGBA: return {"DXT3_SRGBA", true, true, sRGB, ttf::cTFBC3_RGBA, tct::DXT3_RGBA}; + case tif::DXT3_RGBA: return {"DXT3_RGBA", true, true, LINEAR, ttf::cTFBC3_RGBA, tct::DXT3_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 {true, true, LINEAR, ttf::cTFETC2_EAC_RG11, tct::EAC_RG11}; + case tif::EAC_RG11: return {"EAC_RG11", true, true, LINEAR, ttf::cTFETC2_EAC_RG11, tct::EAC_RG11}; // Uncompressed formats. - case tif::SRGB8_A8: return {true, false, sRGB, ttf::cTFRGBA32, {}, tt::UBYTE, tf::RGBA}; - case tif::RGBA8: return {true, false, LINEAR, ttf::cTFRGBA32, {}, tt::UBYTE, tf::RGBA}; - case tif::RGB565: return {true, false, LINEAR, ttf::cTFRGB565, {}, tt::USHORT_565, tf::RGB}; - case tif::RGBA4: return {true, false, LINEAR, ttf::cTFRGBA4444, {}, tt::USHORT, tf::RGBA}; + 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 {false}; + 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), @@ -114,20 +203,20 @@ Ktx2Reader::~Ktx2Reader() { delete mTranscoder; } -bool Ktx2Reader::requestFormat(Texture::InternalFormat format) { +Result Ktx2Reader::requestFormat(Texture::InternalFormat format) noexcept { if (!getFinalFormatInfo(format).isSupported) { - return false; + return Result::FORMAT_UNSUPPORTED; } for (Texture::InternalFormat fmt : mRequestedFormats) { if (fmt == format) { - return false; + return Result::FORMAT_ALREADY_REQUESTED; } } mRequestedFormats.push_back(format); - return true; + return Result::SUCCESS; } -void Ktx2Reader::unrequestFormat(Texture::InternalFormat format) { +void Ktx2Reader::unrequestFormat(Texture::InternalFormat format) noexcept { for (auto iter = mRequestedFormats.begin(); iter != mRequestedFormats.end(); ++iter) { if (*iter == format) { mRequestedFormats.erase(iter); @@ -136,15 +225,102 @@ void Ktx2Reader::unrequestFormat(Texture::InternalFormat format) { } } -Texture* Ktx2Reader::load(const uint8_t* data, size_t size, TransferFunction transfer) { - if (!mTranscoder->init(data, size)) { +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 (mTranscoder->get_dfd_transfer_func() == KTX2_KHR_DF_TRANSFER_LINEAR && + 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." @@ -153,7 +329,7 @@ Texture* Ktx2Reader::load(const uint8_t* data, size_t size, TransferFunction tra return nullptr; } - if (mTranscoder->get_dfd_transfer_func() == KTX2_KHR_DF_TRANSFER_SRGB && + 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." @@ -163,7 +339,7 @@ Texture* Ktx2Reader::load(const uint8_t* data, size_t size, TransferFunction tra } // TODO: support cubemaps. For now we use KTX1 for cubemaps because basisu does not support HDR. - if (mTranscoder->get_faces() == 6) { + if (transcoder->get_faces() == 6) { if (!mQuiet) { utils::slog.e << "Cubemaps are not yet supported." << utils::io::endl; } @@ -171,32 +347,33 @@ Texture* Ktx2Reader::load(const uint8_t* data, size_t size, TransferFunction tra } // TODO: support texture arrays. - if (mTranscoder->get_layers() > 1) { + if (transcoder->get_layers() > 1) { if (!mQuiet) { utils::slog.e << "Texture arrays are not yet supported." << utils::io::endl; } return nullptr; } - // Fierst pass through, just to make sure we can transcode it. + // 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; } - const auto info = getFinalFormatInfo(requestedFormat); + info = getFinalFormatInfo(requestedFormat); if (!info.isSupported || info.transferFunction != transfer) { continue; } - if (!basis_is_format_supported(info.basisFormat, mTranscoder->get_format())) { + 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 < mTranscoder->get_levels(); levelIndex++) { + for (uint32_t levelIndex = 0; levelIndex < transcoder->get_levels(); levelIndex++) { basist::ktx2_image_level_info info; - if (!mTranscoder->get_image_level_info(info, levelIndex, layerIndex, faceIndex)) { + if (!transcoder->get_image_level_info(info, levelIndex, layerIndex, faceIndex)) { continue; } } @@ -212,75 +389,37 @@ Texture* Ktx2Reader::load(const uint8_t* data, size_t size, TransferFunction tra return nullptr; } - const auto formatInfo = getFinalFormatInfo(resolvedFormat); - Texture* texture = Texture::Builder() - .width(mTranscoder->get_width()) - .height(mTranscoder->get_height()) - .levels(mTranscoder->get_levels()) + .width(transcoder->get_width()) + .height(transcoder->get_height()) + .levels(transcoder->get_levels()) .sampler(Texture::Sampler::SAMPLER_2D) .format(resolvedFormat) .build(mEngine); - if (texture == nullptr) { - if (!mQuiet) { - utils::slog.e << "Unable to construct texture using BasisU info." << utils::io::endl; - } - return nullptr; + if (texture == nullptr && !mQuiet) { + utils::slog.e << "Unable to construct texture using BasisU info." << utils::io::endl; } - if (!mTranscoder->start_transcoding()) { - mEngine.destroy(texture); - if (!mQuiet) { - utils::slog.e << "BasisU start_transcoding failed." << utils::io::endl; - } - return nullptr; - } - - // In theory we could pass "free" directly into the callback but that triggers ASAN warnings. - Texture::PixelBufferDescriptor::Callback cb = [](void* buf, size_t, void* userdata) { - free(buf); - }; - - const uint32_t layerIndex = 0; - const uint32_t faceIndex = 0; - for (uint32_t levelIndex = 0; levelIndex < mTranscoder->get_levels(); levelIndex++) { - basist::ktx2_image_level_info levelInfo; - mTranscoder->get_image_level_info(levelInfo, levelIndex, layerIndex, faceIndex); - const basisu::texture_format destFormat = - basis_get_basisu_texture_format(formatInfo.basisFormat); - 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); - const uint32_t flags = 0; - if (!mTranscoder->transcode_image_level(levelIndex, layerIndex, faceIndex, blocks, - levelInfo.m_total_blocks, formatInfo.basisFormat, flags)) { - utils::slog.e << "Failed to transcode level " << levelIndex << utils::io::endl; - return nullptr; - } - Texture::PixelBufferDescriptor pbd(blocks, byteCount, - formatInfo.compressedPixelDataType, byteCount, cb, nullptr); - texture->setImage(mEngine, levelIndex, std::move(pbd)); - } else { - // The transcoder still does work even for uncompressed formats, because of zstd. - 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); - const uint32_t flags = 0; - if (!mTranscoder->transcode_image_level(levelIndex, layerIndex, faceIndex, rows, - byteCount / bytesPerPix, formatInfo.basisFormat, flags)) { - utils::slog.e << "Failed to transcode level " << levelIndex << utils::io::endl; - return nullptr; - } - Texture::PixelBufferDescriptor pbd(rows, byteCount, formatInfo.pixelDataFormat, - formatInfo.pixelDataType, cb, nullptr); - texture->setImage(mEngine, levelIndex, std::move(pbd)); - } - } + #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 diff --git a/third_party/basisu/tnt/CMakeLists.txt b/third_party/basisu/tnt/CMakeLists.txt index 5fe88d735a..042f158bf8 100644 --- a/third_party/basisu/tnt/CMakeLists.txt +++ b/third_party/basisu/tnt/CMakeLists.txt @@ -67,3 +67,4 @@ if (NOT MSVC AND NOT ANDROID) endif() install(TARGETS basisu DESTINATION bin) +install(TARGETS basis_transcoder ARCHIVE DESTINATION lib/${DIST_DIR}) diff --git a/web/filament-js/extensions.js b/web/filament-js/extensions.js index 7e0c58f0ce..11662aa0c2 100644 --- a/web/filament-js/extensions.js +++ b/web/filament-js/extensions.js @@ -146,8 +146,8 @@ Filament.loadClassExtensions = function() { reader.requestFormat(format); } - result = reader.load(buffer, options.srgb ? Filament.TransferFunction.sRGB : - Filament.TransferFunction.LINEAR); + result = reader.load(buffer, options.srgb ? Filament.Ktx2Reader$TransferFunction.sRGB : + Filament.Ktx2Reader$TransferFunction.LINEAR); reader.delete(); buffer.delete(); diff --git a/web/filament-js/filament.d.ts b/web/filament-js/filament.d.ts index e9d1630255..9f003bbbfb 100644 --- a/web/filament-js/filament.d.ts +++ b/web/filament-js/filament.d.ts @@ -1097,6 +1097,19 @@ export enum WrapMode { MIRRORED_REPEAT, } +export enum Ktx2Reader$TransferFunction { + LINEAR, + sRGB, +} + +export enum Ktx2Reader$Result { + SUCCESS, + COMPRESSED_TRANSCODE_FAILURE, + UNCOMPRESSED_TRANSCODE_FAILURE, + FORMAT_UNSUPPORTED, + FORMAT_ALREADY_REQUESTED, +} + export function _malloc(size: number): number; export function _free(size: number): void; diff --git a/web/filament-js/jsenums.cpp b/web/filament-js/jsenums.cpp index bcc86a043f..a910c36287 100644 --- a/web/filament-js/jsenums.cpp +++ b/web/filament-js/jsenums.cpp @@ -421,8 +421,15 @@ enum_("CullingMode") .value("BACK", backend::CullingMode::BACK) .value("FRONT_AND_BACK", backend::CullingMode::FRONT_AND_BACK); -enum_("TransferFunction") +enum_("Ktx2Reader$TransferFunction") .value("LINEAR", ktxreader::Ktx2Reader::TransferFunction::LINEAR) .value("sRGB", ktxreader::Ktx2Reader::TransferFunction::sRGB); +enum_("Ktx2Reader$Result") + .value("SUCCESS", ktxreader::Ktx2Reader::Result::SUCCESS) + .value("COMPRESSED_TRANSCODE_FAILURE", ktxreader::Ktx2Reader::Result::COMPRESSED_TRANSCODE_FAILURE) + .value("UNCOMPRESSED_TRANSCODE_FAILURE", ktxreader::Ktx2Reader::Result::UNCOMPRESSED_TRANSCODE_FAILURE) + .value("FORMAT_UNSUPPORTED", ktxreader::Ktx2Reader::Result::FORMAT_UNSUPPORTED) + .value("FORMAT_ALREADY_REQUESTED", ktxreader::Ktx2Reader::Result::FORMAT_ALREADY_REQUESTED); + }