diff --git a/doc/building-plugins.dox b/doc/building-plugins.dox index 4083dab84..273d4be56 100644 --- a/doc/building-plugins.dox +++ b/doc/building-plugins.dox @@ -227,7 +227,7 @@ enable them, do the following: - For @ref Trade::BasisImporter "BasisImporter" or @ref Trade::BasisImageConverter "BasisImageConverter", [download commit - `2f43afcc` of the Basis Universal repo](https://github.com/BinomialLLC/basis_universal/archive/2f43afcc97d0a5dafdb73b4e24e123cf9687a418.tar.gz), + `77b7df8e` of the Basis Universal repo](https://github.com/BinomialLLC/basis_universal/archive/77b7df8e5df3532a42ef3c76de0c14cc005d0f65.tar.gz), extract it into `src/external/basis-universal` (note the dash instead of an underscore) and set `WITH_BASISIMPORTER` / `WITH_BASISIMAGECONVERTER` to `ON` in `package/debian/rules` diff --git a/modules/FindBasisUniversal.cmake b/modules/FindBasisUniversal.cmake index a914e45bc..ead53af14 100644 --- a/modules/FindBasisUniversal.cmake +++ b/modules/FindBasisUniversal.cmake @@ -31,6 +31,7 @@ # Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, # 2020, 2021 Vladimír Vondruš # Copyright © 2019 Jonathan Hale +# Copyright © 2021 Pablo Escobar # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), @@ -51,13 +52,50 @@ # DEALINGS IN THE SOFTWARE. # +# Several places in this find module assume that the C language is enabled: +# - test_big_endian() assumes C is enabled, configuration fails without. +# CMake says to call enable_language() in the highest directory using the +# language, so we can't do that here. +# - both the transcoder and encoder link to .c files that would just not be +# compiled without the language enabled, or the source file language being +# changed to CXX +# Currently both BasisImporter and BasisImageConverter call enable_language(C). + list(FIND BasisUniversal_FIND_COMPONENTS "Encoder" _index) if(${_index} GREATER -1) list(APPEND BasisUniversal_FIND_COMPONENTS "Transcoder") list(REMOVE_DUPLICATES BasisUniversal_FIND_COMPONENTS) endif() +# Figure out endianness for Basis Universal. test_big_endian() fails on +# Emscripten, but WebAssembly is always little-endian. On CMake 3.8 and below, +# test_big_endian() requires C support which breaks compilation in funny ways +# (see comment below) so we skip that. +if(NOT CORRADE_TARGET_EMSCRIPTEN AND NOT CMAKE_VERSION VERSION_LESS 3.9) + include(TestBigEndian) + test_big_endian(BIG_ENDIAN) +endif() + macro(_basis_setup_source_file source) + # Compile any .c files as C++. Otherwise the files are just ignored because + # the C language is not enabled by project() or enable_language(). Calling + # enable_language in a find module is not a good idea, and even if we do + # this higher up in one of the Basis* plugins, some compilers will require + # the C99 standard being enabled and/or complain about -std=c++11 (done by + # CORRADE_CXX_STANDARD) being set on a C compiler. Doing both (enabling C + # and setting LANGUAGE CXX) still results in static libraries compiling + # with C and producing above-mentioned errors, possibly to do with + # LINKER_LANGUAGE. What a horrible mess. + set_property(SOURCE ${source} PROPERTY LANGUAGE + CXX) + + # Tell Basis if we're on a big endian system. It currently doesn't figure + # this out by itself. + if(BIG_ENDIAN) + set_property(SOURCE ${source} APPEND PROPERTY COMPILE_DEFINITIONS + BASISD_IS_BIG_ENDIAN=1) + endif() + # Basis shouldn't override the MSVC iterator debug level as it would make # it inconsistent with the rest of the code if(CORRADE_TARGET_WINDOWS) @@ -73,7 +111,7 @@ macro(_basis_setup_source_file source) " -w") # Clang supports -w, but it doesn't have any effect on all the # -Wall -Wold-style-cast etc flags specified before. -Wno-everything does. - # Funnily enough this is not an issue on Emscripten.; + # Funnily enough this is not an issue on Emscripten. elseif(CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?Clang" AND NOT CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC") set_property(SOURCE ${source} APPEND_STRING PROPERTY COMPILE_FLAGS " -Wno-everything") @@ -125,20 +163,30 @@ foreach(_component ${BasisUniversal_FIND_COMPONENTS}) "Set BASIS_UNIVERSAL_DIR to the root of a directory containing basis_universal source.") endif() + # @todo Disable file loading at compile time and get rid of the + # BMP/JPG/PNG libraries, we don't use those at all. Hopefully + # this becomes a preprocessor define upstream at some point. + # Alternatively, look into creating stubs for the library + # functions used by basis_universal. set(BasisUniversalEncoder_SOURCES + ${BasisUniversalEncoder_DIR}/apg_bmp.c ${BasisUniversalEncoder_DIR}/basisu_astc_decomp.cpp ${BasisUniversalEncoder_DIR}/basisu_backend.cpp ${BasisUniversalEncoder_DIR}/basisu_basis_file.cpp + ${BasisUniversalEncoder_DIR}/basisu_bc7enc.cpp ${BasisUniversalEncoder_DIR}/basisu_comp.cpp ${BasisUniversalEncoder_DIR}/basisu_enc.cpp ${BasisUniversalEncoder_DIR}/basisu_etc.cpp ${BasisUniversalEncoder_DIR}/basisu_frontend.cpp ${BasisUniversalEncoder_DIR}/basisu_global_selector_palette_helpers.cpp ${BasisUniversalEncoder_DIR}/basisu_gpu_texture.cpp + ${BasisUniversalEncoder_DIR}/basisu_kernels_sse.cpp ${BasisUniversalEncoder_DIR}/basisu_pvrtc1_4.cpp ${BasisUniversalEncoder_DIR}/basisu_resampler.cpp ${BasisUniversalEncoder_DIR}/basisu_resample_filters.cpp ${BasisUniversalEncoder_DIR}/basisu_ssim.cpp + ${BasisUniversalEncoder_DIR}/basisu_uastc_enc.cpp + ${BasisUniversalEncoder_DIR}/jpgd.cpp ${BasisUniversalEncoder_DIR}/lodepng.cpp) foreach(_file ${BasisUniversalEncoder_SOURCES}) @@ -173,8 +221,6 @@ foreach(_component ${BasisUniversal_FIND_COMPONENTS}) # The rest is documented in the BasisImageConverter plugin itself. set_property(TARGET BasisUniversal::Encoder APPEND PROPERTY INTERFACE_LINK_LIBRARIES BasisUniversal::Transcoder) - set_property(TARGET BasisUniversal::Encoder APPEND PROPERTY - INTERFACE_COMPILE_DEFINITIONS "BASISU_NO_ITERATOR_DEBUG_LEVEL") endif() else() set(BasisUniversal_Encoder_FOUND TRUE) @@ -227,6 +273,27 @@ foreach(_component ${BasisUniversal_FIND_COMPONENTS}) set(BasisUniversalTranscoder_SOURCES ${BasisUniversalTranscoder_DIR}/basisu_transcoder.cpp) + set(BasisUniversalTranscoder_DEFINITIONS "BASISU_NO_ITERATOR_DEBUG_LEVEL") + + # Not linking to zstddeclib.c because together with Encoder + # linking to zstd.c this would lead to duplicate symbol + # errors. + # @todo Unused functions *should* be removed by LTO but is + # there a better way? + find_path(BasisUniversalZstd_DIR NAMES zstd.c + HINTS "${BASIS_UNIVERSAL_DIR}/zstd" "${BASIS_UNIVERSAL_DIR}" + NO_CMAKE_FIND_ROOT_PATH) + if(BasisUniversalZstd_DIR) + list(APPEND BasisUniversalTranscoder_SOURCES + ${BasisUniversalZstd_DIR}/zstd.c) + else() + # If zstd wasn't found, disable Zstandard supercompression + # support at compile time. The zstd.h include is hidden + # behind this definition as well. + list(APPEND BasisUniversalTranscoder_DEFINITIONS + "BASISD_SUPPORT_KTX2_ZSTD=0") + endif() + foreach(_file ${BasisUniversalTranscoder_SOURCES}) _basis_setup_source_file(${_file}) endforeach() @@ -243,7 +310,7 @@ foreach(_component ${BasisUniversal_FIND_COMPONENTS}) set_property(TARGET BasisUniversal::Transcoder APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${BasisUniversalTranscoder_INCLUDE_DIR}) set_property(TARGET BasisUniversal::Transcoder APPEND PROPERTY - INTERFACE_COMPILE_DEFINITIONS "BASISU_NO_ITERATOR_DEBUG_LEVEL") + INTERFACE_COMPILE_DEFINITIONS ${BasisUniversalTranscoder_DEFINITIONS}) set_property(TARGET BasisUniversal::Transcoder APPEND PROPERTY INTERFACE_SOURCES "${BasisUniversalTranscoder_SOURCES}") endif() diff --git a/package/archlinux/magnum-plugins-git/PKGBUILD b/package/archlinux/magnum-plugins-git/PKGBUILD index f0fc76f02..3370abba8 100644 --- a/package/archlinux/magnum-plugins-git/PKGBUILD +++ b/package/archlinux/magnum-plugins-git/PKGBUILD @@ -1,7 +1,7 @@ # Author: mosra pkgname=magnum-plugins-git pkgver=2020.06.r119.g15b8cac9 -_basis_pkgver=2f43afcc97d0a5dafdb73b4e24e123cf9687a418 +_basis_pkgver=1_15_update2 pkgrel=1 pkgdesc="Plugins for the Magnum C++11/C++14 graphics engine (Git version)" arch=('i686' 'x86_64') @@ -12,11 +12,9 @@ makedepends=('cmake' 'git' 'ninja') provides=('magnum-plugins') conflicts=('magnum-plugins') source=("git+git://github.com/mosra/magnum-plugins.git" - # A commit that's before the UASTC support (which is not implemented - # yet, because latest versions crash even on trivial tests) - "https://github.com/BinomialLLC/basis_universal/archive/${_basis_pkgver}.tar.gz") + "https://github.com/BinomialLLC/basis_universal/archive/v${_basis_pkgver}.tar.gz") sha1sums=('SKIP' - 'b8d3995292c2c0bbedea943250087b0a9a92ca96') + 'b9615d48ebfc62a53f333ebf8a582558a058b0e9') pkgver() { cd "$srcdir/${pkgname%-git}" diff --git a/package/ci/appveyor.yml b/package/ci/appveyor.yml index 8f9eccc08..822bf1f02 100644 --- a/package/ci/appveyor.yml +++ b/package/ci/appveyor.yml @@ -82,12 +82,9 @@ install: - IF "%TARGET%" == "desktop" IF "%APPVEYOR_BUILD_WORKER_IMAGE%" == "Visual Studio 2017" IF "%COMPILER:~0,4%" == "msvc" appveyor DownloadFile https://ci.magnum.graphics/freetype-2.10.4-windows-2016.zip && 7z x freetype-2.10.4-windows-2016.zip -o%APPVEYOR_BUILD_FOLDER%\deps # Basis Universal -- set BASIS_VERSION=8565af680d1bd2ad56ab227ca7d96c56dfbe93ed -- IF NOT EXIST %APPVEYOR_BUILD_FOLDER%\basis_universal-%BASIS_VERSION%.zip appveyor DownloadFile https://github.com/BinomialLLC/basis_universal/archive/%BASIS_VERSION%.zip -- 7z x %BASIS_VERSION%.zip && ren basis_universal-%BASIS_VERSION% basis_universal -# https://github.com/BinomialLLC/basis_universal/pull/106 -# TODO: remove once we update to UASTC-enabled Basis -- bash -c "cd basis_universal && patch -p1 < ../package/ci/basisu-msvc2019-16.6.patch" +- set BASIS_VERSION=1_15_update2 +- IF NOT EXIST %APPVEYOR_BUILD_FOLDER%\v%BASIS_VERSION%.zip appveyor DownloadFile https://github.com/BinomialLLC/basis_universal/archive/v%BASIS_VERSION%.zip +- 7z x v%BASIS_VERSION%.zip && ren basis_universal-%BASIS_VERSION% basis_universal # SPIRV-Tools, for MSVC 2019, 2017 and clang-cl only # This line REQUIRES the COMPILER variable to be set on rt builds, otherwise it diff --git a/package/ci/basisu-msvc2019-16.6.patch b/package/ci/basisu-msvc2019-16.6.patch deleted file mode 100644 index 564d087a1..000000000 --- a/package/ci/basisu-msvc2019-16.6.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/basisu_enc.h b/basisu_enc.h -index c2b9133..0a0c3c6 100644 ---- a/basisu_enc.h -+++ b/basisu_enc.h -@@ -22,6 +22,7 @@ - #include - #include - #include -+#include - - #if !defined(_WIN32) || defined(__MINGW32__) - #include diff --git a/package/ci/circleci.yml b/package/ci/circleci.yml index 807450ec3..b91ed41ac 100644 --- a/package/ci/circleci.yml +++ b/package/ci/circleci.yml @@ -148,9 +148,8 @@ commands: steps: - run: name: Install Basis Universal - # Version before UASTC is a thing, as our tests crash with the new one command: | - export BASIS_VERSION=8565af680d1bd2ad56ab227ca7d96c56dfbe93ed + export BASIS_VERSION=v1_15_update2 mkdir -p $HOME/basis_universal && cd $HOME/basis_universal wget -nc https://github.com/BinomialLLC/basis_universal/archive/$BASIS_VERSION.tar.gz tar --strip-components 1 -xzf $BASIS_VERSION.tar.gz diff --git a/package/ci/travis.yml b/package/ci/travis.yml index ef2b05496..787e5ebfa 100644 --- a/package/ci/travis.yml +++ b/package/ci/travis.yml @@ -196,7 +196,7 @@ install: - if [ "$TRAVIS_OS_NAME" == "linux" ] && [ "$TARGET" == "desktop-sanitizers" ]; then cd $HOME ; wget https://ci.magnum.graphics/spirv-tools-2020.4-ubuntu-16.04-gcc5.zip && cd $HOME/deps && unzip $HOME/spirv-tools-2020.4-ubuntu-16.04-gcc5.zip && cd $TRAVIS_BUILD_DIR ; fi # Basis Universal -- export BASIS_VERSION=8565af680d1bd2ad56ab227ca7d96c56dfbe93ed && wget -nc https://github.com/BinomialLLC/basis_universal/archive/$BASIS_VERSION.zip && unzip -q $BASIS_VERSION; mv basis_universal-$BASIS_VERSION $HOME/basis_universal +- export BASIS_VERSION=1_15_update2 && wget -nc https://github.com/BinomialLLC/basis_universal/archive/v$BASIS_VERSION.zip && unzip -q v$BASIS_VERSION; mv basis_universal-$BASIS_VERSION $HOME/basis_universal script: - if [ "$TRAVIS_OS_NAME" == "linux" ] && ( [ "$TARGET" == "desktop" ] || [ "$TARGET" == "desktop-sanitizers" ] ); then ./package/ci/unix-desktop.sh; fi diff --git a/package/homebrew/magnum-plugins.rb b/package/homebrew/magnum-plugins.rb index acc31cd2d..2cb146c4b 100644 --- a/package/homebrew/magnum-plugins.rb +++ b/package/homebrew/magnum-plugins.rb @@ -23,11 +23,16 @@ class MagnumPlugins < Formula depends_on "spirv-tools" => :recommended def install - # Bundle Basis Universal, a commit that's before the UASTC support (which - # is not implemented yet). The repo has massive useless files in its - # history, so we're downloading just a snapshot instead of a git clone. - # Also, WHY THE FUCK curl needs -L and -o?! why can't it just work?! - system "curl", "-L", "https://github.com/BinomialLLC/basis_universal/archive/2f43afcc97d0a5dafdb73b4e24e123cf9687a418.tar.gz", "-o", "src/external/basis-universal.tar.gz" + # Bundle Basis Universal, v1_15_update2 for HEAD builds, a commit that's + # before the UASTC support (which was not implemented yet) on 2020.06. + # The repo has massive useless files in its history, so we're downloading + # just a snapshot instead of a git clone. Also, WHY THE FUCK curl needs -L + # and -o?! why can't it just work?! + if build.head? + system "curl", "-L", "https://github.com/BinomialLLC/basis_universal/archive/v1_15_update2.tar.gz", "-o", "src/external/basis-universal.tar.gz" + else + system "curl", "-L", "https://github.com/BinomialLLC/basis_universal/archive/2f43afcc97d0a5dafdb73b4e24e123cf9687a418.tar.gz", "-o", "src/external/basis-universal.tar.gz" + end cd "src/external" do system "mkdir", "basis-universal" system "tar", "xzvf", "basis-universal.tar.gz", "-C", "basis-universal", "--strip-components=1" diff --git a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf index 02495bac6..beb4c140b 100644 --- a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf +++ b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf @@ -1,17 +1,20 @@ +provides=BasisKtxImageConverter + # [configuration_] [configuration] -# All following options correspond to options of the basisu tool, grouped in -# the same way. Names follow the Basis C++ API and may differ from what the -# tool exposes. +# All following options correspond to parameters of the `basis_compressor` C++ +# API and may differ from what the basisu tool exposes. # Options quality_level=128 -# sRGB images should have this enabled, turn this flag off for linear images -perceptual=true +# Treat images as sRGB color data, rather than linear intensity. Leave blank to +# determine from the image format. +perceptual= debug=false +validate=false debug_images=false compute_stats=false -compression_level=1 +compression_level=2 # More options max_endpoint_clusters=512 @@ -21,7 +24,14 @@ y_flip=true # `mip_srgb` and enabling `no_selector_rdo` & `no_endpoint_rdo` check_for_alpha=true force_alpha=false -separate_rg_to_color_alpha=false +# Remap color channels before compression. Must be empty or 4 characters long, +# valid characters are r,g,b,a. This replaced separate_rg_to_color_alpha, +# for the same effect use 'rrrg'. +swizzle= +renormalize=false +resample_width= +resample_height= +resample_factor= # Number of threads Basis should use during compression, 0 sets it to the # value returned by std::thread::hardware_concurrency(), 1 disables # multithreading. This value is clamped to std::thread::hardware_concurrency() @@ -30,13 +40,18 @@ threads=1 disable_hierarchical_endpoint_codebooks=false # Mipmap generation options +# Generate mipmaps from the base image. If you pass custom mip levels into +# openData, this option will be ignored. Leave blank to determine from the +# number of levels passed to convertToData. mip_gen=false -# Generate mipmaps assuming sRGB input, turn this flag off for linear images -mip_srgb=true +# Filter mipmaps assuming sRGB color data, rather than linear intensity. Leave +# blank to determine from the image format. +mip_srgb= mip_scale=1.0 mip_filter=kaiser mip_renormalize=false mip_wrapping=true +mip_fast=true mip_smallest_dimension=1 # Backend endpoint/selector RDO codec options @@ -53,6 +68,23 @@ global_palette_bits=8 global_modifier_bits=8 hybrid_selector_codebook_quality_threshold=2.0 +# UASTC options +uastc=false +pack_uastc_level=2 +pack_uastc_flags= +rdo_uastc=false +rdo_uastc_quality_scalar=1.0 +rdo_uastc_dict_size=4096 +rdo_uastc_max_smooth_block_error_scale=10.0 +rdo_uastc_smooth_block_max_std_dev=18.0 +rdo_uastc_max_allowed_rms_increase_ratio=10.0 +rdo_uastc_skip_block_rms_threshold=8.0 +rdo_uastc_favor_simpler_modes_in_rdo_mode=true + +# KTX2 options +ktx2_uastc_supercompression=true +ktx2_zstd_supercompression_level=6 + # Set various fields in the Basis file header userdata0=0 userdata1=0 diff --git a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp index fadf9a122..9b172c8b6 100644 --- a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp +++ b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp @@ -4,6 +4,7 @@ Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Vladimír Vondruš Copyright © 2019 Jonathan Hale + Copyright © 2021 Pablo Escobar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -31,46 +32,93 @@ #include #include #include -#include #include #include #include +#include +#include #include #include #include #include + #include #include #include namespace Magnum { namespace Trade { -BasisImageConverter::BasisImageConverter() = default; +using namespace Containers::Literals; -BasisImageConverter::BasisImageConverter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImageConverter{manager, plugin} {} +BasisImageConverter::BasisImageConverter(Format format): _format{format} { + /* Passing an invalid Format enum is user error, we'll assert on that in + the convertToData() function */ +} -ImageConverterFeatures BasisImageConverter::doFeatures() const { return ImageConverterFeature::Convert2DToData; } +BasisImageConverter::BasisImageConverter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImageConverter{manager, plugin} { + if(plugin == "BasisKtxImageConverter") + _format = Format::Ktx; + else + _format = {}; /* Overridable by openFile() */ +} -Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& image) { +ImageConverterFeatures BasisImageConverter::doFeatures() const { return ImageConverterFeature::ConvertLevels2DToData; } + +Containers::Array BasisImageConverter::doConvertToData(Containers::ArrayView imageLevels) { /* Check input */ - if(image.format() != PixelFormat::RGB8Unorm && - image.format() != PixelFormat::RGBA8Unorm && - image.format() != PixelFormat::RG8Unorm && - image.format() != PixelFormat::R8Unorm) - { - Error{} << "Trade::BasisImageConverter::convertToData(): unsupported format" << image.format(); + const PixelFormat imageFormat = imageLevels.front().format(); + bool isSrgb; + switch(imageFormat) { + case PixelFormat::RGBA8Unorm: + case PixelFormat::RGB8Unorm: + case PixelFormat::RG8Unorm: + case PixelFormat::R8Unorm: + isSrgb = false; + break; + case PixelFormat::RGBA8Srgb: + case PixelFormat::RGB8Srgb: + case PixelFormat::RG8Srgb: + case PixelFormat::R8Srgb: + isSrgb = true; + break; + default: + Error{} << "Trade::BasisImageConverter::convertToData(): unsupported format" << imageFormat; + return {}; + } + + const Vector2i size = imageLevels.front().size(); + + const UnsignedInt numMipmaps = Math::min(imageLevels.size(), Math::log2(size.max()) + 1); + if(imageLevels.size() > numMipmaps) { + Error{} << "Trade::BasisImageConverter::convertToData(): there can be only" << numMipmaps << + "levels with base image size" << imageLevels.front().size() << "but got" << imageLevels.size(); return {}; } + basisu::basis_compressor_params params; + + if(_format == Format::Ktx) + params.m_create_ktx2_file = true; + else + CORRADE_INTERNAL_ASSERT(_format == Format{} || _format == Format::Basis); + + /* Options deduced from input data. Config values that are not emptied out + override these below. */ + params.m_perceptual = isSrgb; + params.m_mip_gen = imageLevels.size() == 1; + params.m_mip_srgb = isSrgb; + /* To retain sanity, keep this in the same order and grouping as in the conf file */ - basisu::basis_compressor_params params; - #define PARAM_CONFIG(name, type) params.m_##name = configuration().value(#name) - #define PARAM_CONFIG_FIX_NAME(name, type, fixed) params.m_##name = configuration().value(fixed) + #define PARAM_CONFIG(name, type) \ + if(!configuration().value(#name).empty()) params.m_##name = configuration().value(#name) + #define PARAM_CONFIG_FIX_NAME(name, type, fixed) \ + if(!configuration().value(fixed).empty()) params.m_##name = configuration().value(fixed) /* Options */ PARAM_CONFIG(quality_level, int); PARAM_CONFIG(perceptual, bool); PARAM_CONFIG(debug, bool); + PARAM_CONFIG(validate, bool); PARAM_CONFIG(debug_images, bool); PARAM_CONFIG(compute_stats, bool); PARAM_CONFIG(compression_level, int); @@ -81,7 +129,34 @@ Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& PARAM_CONFIG(y_flip, bool); PARAM_CONFIG(check_for_alpha, bool); PARAM_CONFIG(force_alpha, bool); - PARAM_CONFIG_FIX_NAME(seperate_rg_to_color_alpha, bool, "separate_rg_to_color_alpha"); + + const std::string swizzle = configuration().value("swizzle"); + if(!swizzle.empty()) { + if(swizzle.size() != 4) { + Error{} << "Trade::BasisImageConverter::convertToData(): invalid swizzle length, expected 4 but got" << swizzle.size(); + return {}; + } + + if(swizzle.find_first_not_of("rgba") != std::string::npos) { + Error{} << "Trade::BasisImageConverter::convertToData(): invalid characters in swizzle" << swizzle; + return {}; + } + + for(std::size_t i = 0; i != 4; ++i) { + switch(swizzle[i]) { + case 'r': params.m_swizzle[i] = 0; break; + case 'g': params.m_swizzle[i] = 1; break; + case 'b': params.m_swizzle[i] = 2; break; + case 'a': params.m_swizzle[i] = 3; break; + default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + } + } + + PARAM_CONFIG(renormalize, bool); + PARAM_CONFIG(resample_width, int); + PARAM_CONFIG(resample_height, int); + PARAM_CONFIG(resample_factor, float); UnsignedInt threadCount = configuration().value("threads"); if(threadCount == 0) threadCount = std::thread::hardware_concurrency(); @@ -99,6 +174,7 @@ Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& PARAM_CONFIG(mip_filter, std::string); PARAM_CONFIG(mip_renormalize, bool); PARAM_CONFIG(mip_wrapping, bool); + PARAM_CONFIG(mip_fast, bool); PARAM_CONFIG(mip_smallest_dimension, int); /* Backend endpoint/selector RDO codec options */ @@ -115,58 +191,142 @@ Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& PARAM_CONFIG_FIX_NAME(global_mod_bits, int, "global_modifier_bits"); PARAM_CONFIG_FIX_NAME(hybrid_sel_cb_quality_thresh, float, "hybrid_sel_codebook_quality_threshold"); + /* UASTC options */ + PARAM_CONFIG(uastc, bool); + params.m_pack_uastc_flags = configuration().value("pack_uastc_level"); + PARAM_CONFIG(pack_uastc_flags, int); + PARAM_CONFIG(rdo_uastc, bool); + PARAM_CONFIG(rdo_uastc_quality_scalar, float); + PARAM_CONFIG(rdo_uastc_dict_size, int); + PARAM_CONFIG(rdo_uastc_max_smooth_block_error_scale, float); + PARAM_CONFIG(rdo_uastc_smooth_block_max_std_dev, float); + PARAM_CONFIG(rdo_uastc_max_allowed_rms_increase_ratio, float); + PARAM_CONFIG_FIX_NAME(rdo_uastc_skip_block_rms_thresh, float, "rdo_uastc_skip_block_rms_threshold"); + PARAM_CONFIG(rdo_uastc_favor_simpler_modes_in_rdo_mode, bool); + params.m_rdo_uastc_multithreading = multithreading; + + /* KTX2 options */ + params.m_ktx2_uastc_supercompression = + configuration().value("ktx2_uastc_supercompression") ? basist::KTX2_SS_ZSTANDARD : basist::KTX2_SS_NONE; + PARAM_CONFIG(ktx2_zstd_supercompression_level, int); + params.m_ktx2_srgb_transfer_func = params.m_perceptual; + + /* y_flip sets a flag in Basis files, but not in KTX2 files: + https://github.com/BinomialLLC/basis_universal/issues/258 + Manually specify the orientation in the key/value data: + https://www.khronos.org/registry/KTX/specs/2.0/ktxspec_v2.html#_ktxorientation */ + constexpr char OrientationKey[] = "KTXorientation"; + char orientationValue[] = "rd"; + if(params.m_y_flip) + orientationValue[1] = 'u'; + basist::ktx2_transcoder::key_value& keyValue = *params.m_ktx2_key_values.enlarge(1); + keyValue.m_key.append(reinterpret_cast(OrientationKey), sizeof(OrientationKey)); + keyValue.m_key.append(reinterpret_cast(orientationValue), sizeof(orientationValue)); + /* Set various fields in the Basis file header */ PARAM_CONFIG(userdata0, int); PARAM_CONFIG(userdata1, int); #undef PARAM_CONFIG #undef PARAM_CONFIG_FIX_NAME - /* If these are enabled, the library reads PNGs from a filesystem and then - writes basis files there also. DO NOT WANT. */ + /* Don't spam stdout with debug info by default. Basis error output is + unaffected by this. Unfortunately, there's no way to redirect the output + to Debug. */ + params.m_status_output = flags() >= ImageConverterFlag::Verbose; + + /* If these are enabled, the library reads BMPs/JPGs/PNGs/TGAs from the + filesystem and then writes basis files there also. DO NOT WANT. */ params.m_read_source_images = false; params.m_write_output_basis_files = false; - basist::etc1_global_selector_codebook sel_codebook(basist::g_global_selector_cb_size, basist::g_global_selector_cb); + const basist::etc1_global_selector_codebook sel_codebook(basist::g_global_selector_cb_size, basist::g_global_selector_cb); params.m_pSel_codebook = &sel_codebook; - /* Copy image data into the basis image. There is no way to construct a - basis image from existing data as it is based on a std::vector, moreover - we need to tightly pack it and flip Y. The `dst` is an Y-flipped view - already to make the following loops simpler. */ - params.m_source_images.emplace_back(image.size().x(), image.size().y()); - auto dst = Containers::arrayCast(Containers::StridedArrayView2D({params.m_source_images.back().get_ptr(), params.m_source_images.back().get_total_pixels()}, {std::size_t(image.size().y()), std::size_t(image.size().x())})).flipped<0>(); - - /* basis image is always RGBA, fill in alpha if necessary */ - if(image.format() == PixelFormat::RGBA8Unorm) { - auto src = image.pixels>(); - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = src[y][x]; - - } else if(image.format() == PixelFormat::RGB8Unorm) { - auto src = image.pixels>(); - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = src[y][x]; /* Alpha implicitly 255 */ - - } else if(image.format() == PixelFormat::RG8Unorm) { - auto src = image.pixels>(); - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Math::gather<'r', 'r', 'r', 'g'>(src[y][x]); - - } else if(image.format() == PixelFormat::R8Unorm) { - auto src = image.pixels>(); - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Math::gather<'r', 'r', 'r'>(src[y][x]); - - } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + /* The base mip is in m_source_images, mip 1 and higher go into + m_source_mipmap_images. If m_source_mipmap_images is not empty, mip + generation is disabled. */ + params.m_source_images.resize(1); + if(imageLevels.size() > 1) { + if(params.m_mip_gen) { + Warning{} << "Trade::BasisImageConverter::convertToData(): found user-supplied mip levels, ignoring mip_gen config value"; + params.m_mip_gen = false; + } + + params.m_source_mipmap_images.resize(1); + params.m_source_mipmap_images[0].resize(imageLevels.size() - 1); + } + + for(UnsignedInt i = 0; i != imageLevels.size(); ++i) { + const Vector2i mipSize = Math::max(size >> i, 1); + const auto& image = imageLevels[i]; + + if(image.size() != mipSize) { + Error{} << "Trade::BasisImageConverter::convertToData(): expected " + "size" << mipSize << "for level" << i << "but got" << image.size(); + return {}; + } + + /* Copy image data into the basis image. There is no way to construct a + basis image from existing data as it is based on basisu::vector, + moreover we need to tightly pack it and flip Y. */ + basisu::image& basisImage = i == 0 ? params.m_source_images[0] : params.m_source_mipmap_images[0][i - 1]; + basisImage.resize(image.size().x(), image.size().y()); + auto dst = Containers::arrayCast(Containers::StridedArrayView2D({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(image.size().y()), std::size_t(image.size().x())})); + /* Y-flip the view to make the following loops simpler. basisu doesn't + apply m_y_flip to user-supplied mipmaps, so only do this for the + base image: + https://github.com/BinomialLLC/basis_universal/issues/257 */ + if(!params.m_y_flip || i == 0) + dst = dst.flipped<0>(); + + /* basis image is always RGBA, fill in alpha if necessary */ + const UnsignedInt channels = pixelSize(imageFormat); + if(channels == 4) { + auto src = image.pixels>(); + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = src[y][x]; + + } else if(channels == 3) { + auto src = image.pixels>(); + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = src[y][x]; /* Alpha implicitly 255 */ + + } else if(channels == 2) { + auto src = image.pixels>(); + /* If the user didn't specify a custom swizzle, assume they want + the two channels compressed in separate slices, R in RGB and G + in Alpha. This significantly improves quality. */ + if(swizzle.empty()) + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::gather<'r', 'r', 'r', 'g'>(src[y][x]); + else + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Vector3ub::pad(src[y][x]); /* Alpha implicitly 255 */ + + } else if(channels == 1) { + auto src = image.pixels>(); + /* If the user didn't specify a custom swizzle, assume they want + a gray-scale image. Alpha is always implicitly 255. */ + if(swizzle.empty()) + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::gather<'r', 'r', 'r'>(src[y][x]); + else + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Vector3ub::pad(src[y][x]); + + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + } basisu::basis_compressor basis; basis.init(params); - basisu::basis_compressor::error_code errorCode = basis.process(); + const basisu::basis_compressor::error_code errorCode = basis.process(); if(errorCode != basisu::basis_compressor::error_code::cECSuccess) switch(errorCode) { case basisu::basis_compressor::error_code::cECFailedReadingSourceImages: /* Emitted e.g. when source image is 0-size */ @@ -176,6 +336,9 @@ Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& /* process() will have printed additional error information to stderr */ Error{} << "Trade::BasisImageConverter::convertToData(): type constraint validation failed"; return {}; + case basisu::basis_compressor::error_code::cECFailedEncodeUASTC: + Error{} << "Trade::BasisImageConverter::convertToData(): UASTC encoding failed"; + return {}; case basisu::basis_compressor::error_code::cECFailedFrontEnd: /* process() will have printed additional error information to stderr */ Error{} << "Trade::BasisImageConverter::convertToData(): frontend processing failed"; @@ -187,6 +350,12 @@ Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& /* process() will have printed additional error information to stderr */ Error{} << "Trade::BasisImageConverter::convertToData(): assembling basis file data or transcoding failed"; return {}; + case basisu::basis_compressor::error_code::cECFailedUASTCRDOPostProcess: + Error{} << "Trade::BasisImageConverter::convertToData(): UASTC RDO postprocessing failed"; + return {}; + case basisu::basis_compressor::error_code::cECFailedCreateKTX2File: + Error{} << "Trade::BasisImageConverter::convertToData(): assembling KTX2 file failed"; + return {}; /* LCOV_EXCL_START */ case basisu::basis_compressor::error_code::cECFailedFontendExtract: @@ -199,14 +368,36 @@ Containers::Array BasisImageConverter::doConvertToData(const ImageView2D& /* LCOV_EXCL_STOP */ } - const basisu::uint8_vec& out = basis.get_output_basis_file(); + const basisu::uint8_vec& out = params.m_create_ktx2_file ? basis.get_output_ktx2_file() : basis.get_output_basis_file(); Containers::Array fileData{NoInit, out.size()}; - Utility::copy(Containers::arrayCast(out), fileData); + Utility::copy(Containers::arrayCast(Containers::arrayView(out.data(), out.size())), fileData); return fileData; } +bool BasisImageConverter::doConvertToFile(const Containers::ArrayView imageLevels, const Containers::StringView filename) { + /** @todo once Directory is std::string-free, use splitExtension() */ + const Containers::String normalized = Utility::String::lowercase(filename); + + /* Save the previous format to restore it back after, detect the format + from extension if it's not supplied explicitly */ + const Format previousFormat = _format; + if(_format == Format{}) { + if(normalized.hasSuffix(".ktx2"_s)) + _format = Format::Ktx; + else + _format = Format::Basis; + } + + /* Delegate to the base implementation which calls doConvertToData() */ + const bool out = AbstractImageConverter::doConvertToFile(imageLevels, filename); + + /* Restore the previous format and return the result */ + _format = previousFormat; + return out; +} + }} CORRADE_PLUGIN_REGISTER(BasisImageConverter, Magnum::Trade::BasisImageConverter, diff --git a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h index d4afc46a9..44d1a592d 100644 --- a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h +++ b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h @@ -6,6 +6,7 @@ Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Vladimír Vondruš Copyright © 2019 Jonathan Hale + Copyright © 2021 Pablo Escobar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -57,11 +58,17 @@ namespace Magnum { namespace Trade { @brief Basis Universal image converter plugin @m_since_{plugins,2019,10} +@m_keywords{BasisKtxImageConverter} + Creates [Basis Universal](https://github.com/binomialLLC/basis_universal) -(`*.basis`) files from images with format @ref PixelFormat::R8Unorm, -@ref PixelFormat::RG8Unorm, @ref PixelFormat::RGB8Unorm or -@ref PixelFormat::RGBA8Unorm. Use @ref BasisImporter to import images in this -format. +compressed image files (`*.basis` or `*.ktx2`) from images with format +@ref PixelFormat::R8Unorm, @ref PixelFormat::R8Srgb, +@ref PixelFormat::RG8Unorm, @ref PixelFormat::RG8Srgb, +@ref PixelFormat::RGB8Unorm, @ref PixelFormat::RGB8Srgb, +@ref PixelFormat::RGBA8Unorm or @ref PixelFormat::RGBA8Srgb. +Use @ref BasisImporter to import images in this format. + +This plugin provides `BasisKtxImageConverter`. @m_class{m-block m-success} @@ -77,7 +84,9 @@ format. This plugin depends on the @ref Trade and [Basis Universal](https://github.com/binomialLLC/basis_universal) libraries and is built if `WITH_BASISIMAGECONVERTER` is enabled when building Magnum Plugins. To use as a dynamic plugin, load @cpp "BasisImageConverter" @ce -via @ref Corrade::PluginManager::Manager. +via @ref Corrade::PluginManager::Manager. Current version of the plugin is +tested against the [`v1_15_update2` tag](https://github.com/BinomialLLC/basis_universal/tree/v1_15_update2), +but could possibly compile against newer versions as well. Additionally, if you're using Magnum as a CMake subproject, bundle the [magnum-plugins](https://github.com/mosra/magnum-plugins) and @@ -112,17 +121,40 @@ See @ref building-plugins, @ref cmake-plugins, @ref plugins and @section Trade-BasisImageConverter-behavior Behavior and limitations -@subsection Trade-BasisImageConverter-behavior-multiple-images Multiple images in one file +@subsection Trade-BasisImageConverter-behavior-multilevel Multilevel images -Due to limitations in the @ref AbstractImageConverter API, it's currently not -possible to create a Basis file containing multiple images --- you'd need to -use the upstream `basisu` tool for that instead. +Images can be saved with multiple levels by using the list variants of +@ref convertToFile() / @ref convertToData(). Largest level is expected to be +first, with each following level having width and height divided by two, +rounded down. Incomplete mip chains are supported. -Supplying custom mip levels will be possible when the converter gets updated to -[the upcoming version 1.16](https://github.com/BinomialLLC/basis_universal/commit/ee626cec19e8e2d206bfc127296dfd9519352dc6). Right now, there's only a -possibility to generate the mip levels from the top-level image using the +To generate mip levels from a single top-level image instead, you can use the @cb{.ini} mip_gen @ce @ref Trade-BasisImageConverter-configuration "configuration option". +@subsection Trade-BasisImageConverter-behavior-swizzling Implicit swizzling + +If no user-specified channel mapping is supplied through the +@cb{.ini} swizzle @ce @ref Trade-BasisImageConverter-configuration "configuration option", +the converter swizzles 1- and 2-channel formats before compression as follows: + +- 1-channel formats (@ref PixelFormat::R8Unorm / @ref PixelFormat::R8Srgb) + are remapped as RRR, producing an opaque gray-scale image +- 2-channel formats (@ref PixelFormat::RG8Unorm / @ref PixelFormat::RG8Srgb) + are remapped as RRRG, ie. G becomes the alpha channel. This significantly + improves compressed image quality because RGB and alpha get separate slices + instead of the two channels being compressed into a single slice. + +To disable this behaviour and keep the original channels, set +@cb{.ini} swizzle @ce to "rgba". + +@subsection Trade-BasisImageConverter-behavior-ktx Converting to KTX2 + +To create Khronos Texture 2.0 (`*.ktx2`) files, either load the plugin as +`BasisKtxImageConverter`, call @ref convertToFile() with the `.ktx2` extension +or pass @ref Format::Ktx to the constructor. + +In all other cases, a Basis Universal (`*.basis`) file is created. + @subsection Trade-BasisImageConverter-behavior-loading Loading the plugin fails with undefined symbol: pthread_create On Linux it may happen that loading the plugin will fail with @@ -144,8 +176,11 @@ target_link_libraries(your-application PRIVATE Threads::Threads) @section Trade-BasisImageConverter-configuration Plugin-specific configuration Basis compression can be configured to produce better quality or reduce -encoding time. Configuration options are equivalent to options of the `basisu` -tool. The full form of the configuration is shown below: +encoding time. Configuration options are equivalent to parameters of the C++ +encoder API in `basis_compressor`. The `basisu` tool options mostly match the +encoder API parameters and its [help text](https://github.com/BinomialLLC/basis_universal/blob/v1_15_update2/basisu_tool.cpp#L76) +provides useful descriptions of most of the parameters, their ranges and the +impact on quality/speed. The full form of the configuration is shown below: @snippet MagnumPlugins/BasisImageConverter/BasisImageConverter.conf configuration_ @@ -154,15 +189,35 @@ to edit the configuration values. */ class MAGNUM_BASISIMAGECONVERTER_EXPORT BasisImageConverter: public AbstractImageConverter { public: - /** @brief Default constructor */ - explicit BasisImageConverter(); + /** + * @brief Output file format + * + * @see @ref BasisImageConverter(Format) + */ + enum class Format: Int { + /* 0 used for default value, Basis unless overridden by + convertToFile */ + + Basis = 1, /**< Output Basis images */ + Ktx, /**< Output KTX2 images */ + }; + + /** + * @brief Default constructor + * + * The converter outputs files in format defined by @ref Format. + */ + explicit BasisImageConverter(Format format = Format{}); /** @brief Plugin manager constructor */ explicit BasisImageConverter(PluginManager::AbstractManager& manager, const std::string& plugin); private: MAGNUM_BASISIMAGECONVERTER_LOCAL ImageConverterFeatures doFeatures() const override; - MAGNUM_BASISIMAGECONVERTER_LOCAL Containers::Array doConvertToData(const ImageView2D& image) override; + MAGNUM_BASISIMAGECONVERTER_LOCAL Containers::Array doConvertToData(Containers::ArrayView imageLevels) override; + MAGNUM_BASISIMAGECONVERTER_LOCAL bool doConvertToFile(const Containers::ArrayView imageLevels, const Containers::StringView filename) override; + + Format _format; }; }} diff --git a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp index 49e4b1177..3853a1c82 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp +++ b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp @@ -28,8 +28,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -51,13 +53,28 @@ struct BasisImageConverterTest: TestSuite::Tester { explicit BasisImageConverterTest(); void wrongFormat(); + void unknownOutputFormatData(); + void unknownOutputFormatFile(); + void invalidSwizzle(); + void tooManyLevels(); + void levelWrongSize(); void processError(); + void configPerceptual(); + void configMipGen(); + void r(); void rg(); void rgb(); void rgba(); + void convertToFile(); + + void threads(); + void ktx(); + void customLevels(); + void swizzle(); + /* Explicitly forbid system-wide plugin dependencies */ PluginManager::Manager _converterManager{"nonexistent"}; @@ -65,27 +82,101 @@ struct BasisImageConverterTest: TestSuite::Tester { PluginManager::Manager _manager; }; +using namespace Containers::Literals; + +enum TransferFunction: std::size_t { + Linear, + Srgb +}; + +constexpr PixelFormat TransferFunctionFormats[2][4]{ + {PixelFormat::R8Unorm, PixelFormat::RG8Unorm, PixelFormat::RGB8Unorm, PixelFormat::RGBA8Unorm}, + {PixelFormat::R8Srgb, PixelFormat::RG8Srgb, PixelFormat::RGB8Srgb, PixelFormat::RGBA8Srgb} +}; + +constexpr struct { + const char* name; + const TransferFunction transferFunction; +} FormatTransferFunctionData[] { + {"Unorm", TransferFunction::Linear}, + {"Srgb", TransferFunction::Srgb} +}; + +constexpr Containers::StringView BasisPrefix = "sB"_s; +constexpr Containers::StringView KtxPrefix = "\xabKTX"_s; + +constexpr struct { + const char* name; + const char* pluginName; + const char* filename; + const Containers::StringView prefix; +} ConvertToFileData[] { + {"Basis", "BasisImageConverter", "image.basis", BasisPrefix}, + {"KTX2", "BasisImageConverter", "image.ktx2", KtxPrefix}, + {"KTX2 with explicit plugin name", "BasisKtxImageConverter", "image.foo", KtxPrefix} +}; + constexpr struct { const char* name; const char* threads; } ThreadsData[] { - {"", nullptr}, {"2 threads", "2"}, {"all threads", "0"} }; +constexpr struct { + const char* name; + const bool yFlip; +} FlippedData[] { + {"y-flip", true}, + {"no y-flip", false} +}; + +constexpr struct { + const char* name; + const PixelFormat format; + const Color4ub input; + const char* swizzle; + const Color4ub output; +} SwizzleData[] { + {"R implicit", PixelFormat::R8Unorm, Color4ub{128, 0, 0}, "", Color4ub{128, 128, 128}}, + {"R none", PixelFormat::R8Unorm, Color4ub{128, 0, 0}, "rgba", Color4ub{128, 0, 0}}, + {"RG implicit", PixelFormat::RG8Unorm, Color4ub{64, 128, 0}, "", Color4ub{64, 64, 64, 128}}, + {"RG none", PixelFormat::RG8Unorm, Color4ub{64, 128, 0}, "rgba", Color4ub{64, 128, 0}} +}; + BasisImageConverterTest::BasisImageConverterTest() { addTests({&BasisImageConverterTest::wrongFormat, + &BasisImageConverterTest::unknownOutputFormatData, + &BasisImageConverterTest::unknownOutputFormatFile, + &BasisImageConverterTest::invalidSwizzle, + &BasisImageConverterTest::tooManyLevels, + &BasisImageConverterTest::levelWrongSize, &BasisImageConverterTest::processError, - &BasisImageConverterTest::r, - &BasisImageConverterTest::rg, + &BasisImageConverterTest::configPerceptual, + &BasisImageConverterTest::configMipGen}); + + addInstancedTests({&BasisImageConverterTest::r, + &BasisImageConverterTest::rg, + &BasisImageConverterTest::rgb, + &BasisImageConverterTest::rgba}, + Containers::arraySize(FormatTransferFunctionData)); - &BasisImageConverterTest::rgb}); + addInstancedTests({&BasisImageConverterTest::convertToFile}, + Containers::arraySize(ConvertToFileData)); - addInstancedTests({&BasisImageConverterTest::rgba}, + addInstancedTests({&BasisImageConverterTest::threads}, Containers::arraySize(ThreadsData)); + addInstancedTests({&BasisImageConverterTest::ktx}, + Containers::arraySize(FlippedData)); + + addTests({&BasisImageConverterTest::customLevels}); + + addInstancedTests({&BasisImageConverterTest::swizzle}, + Containers::arraySize(SwizzleData)); + /* Pull in the AnyImageImporter dependency for image comparison, load StbImageImporter from the build tree, if defined. Otherwise it's static and already loaded. */ @@ -102,11 +193,13 @@ BasisImageConverterTest::BasisImageConverterTest() { #ifdef BASISIMPORTER_PLUGIN_FILENAME CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(BASISIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); #endif + + /* Create the output directory if it doesn't exist yet */ + CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Directory::mkpath(BASISIMAGECONVERTER_TEST_OUTPUT_DIR)); } void BasisImageConverterTest::wrongFormat() { - Containers::Pointer converter = - _converterManager.instantiate("BasisImageConverter"); + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); const char data[8]{}; std::ostringstream out; @@ -115,41 +208,209 @@ void BasisImageConverterTest::wrongFormat() { CORRADE_COMPARE(out.str(), "Trade::BasisImageConverter::convertToData(): unsupported format PixelFormat::RG32F\n"); } +void BasisImageConverterTest::unknownOutputFormatData() { + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); + + /* The converter defaults to .basis output, conversion should succeed */ + + const char data[4]{}; + const auto converted = converter->convertToData(ImageView2D{PixelFormat::RGB8Unorm, {1, 1}, data}); + CORRADE_VERIFY(converted); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openData(converted)); +} + +void BasisImageConverterTest::unknownOutputFormatFile() { + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); + + /* The converter defaults to .basis output, conversion should succeed */ + + const char data[4]{}; + const ImageView2D image{PixelFormat::RGB8Unorm, {1, 1}, data}; + const std::string filename = Utility::Directory::join(BASISIMAGECONVERTER_TEST_OUTPUT_DIR, "file.foo"); + CORRADE_VERIFY(converter->convertToFile(image, filename)); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(filename)); +} + +void BasisImageConverterTest::invalidSwizzle() { + Containers::Pointer converter = + _converterManager.instantiate("BasisImageConverter"); + + const char data[8]{}; + std::ostringstream out; + Error redirectError{&out}; + + converter->configuration().setValue("swizzle", "gbgbg"); + CORRADE_VERIFY(!converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, data})); + + converter->configuration().setValue("swizzle", "xaaa"); + CORRADE_VERIFY(!converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, data})); + + CORRADE_COMPARE(out.str(), + "Trade::BasisImageConverter::convertToData(): invalid swizzle length, expected 4 but got 5\n" + "Trade::BasisImageConverter::convertToData(): invalid characters in swizzle xaaa\n"); +} + +void BasisImageConverterTest::tooManyLevels() { + Containers::Pointer converter = + _converterManager.instantiate("BasisImageConverter"); + + const UnsignedByte bytes[4]{}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter->convertToData({ + ImageView2D{PixelFormat::RGB8Unorm, {1, 1}, bytes}, + ImageView2D{PixelFormat::RGB8Unorm, {1, 1}, bytes} + })); + CORRADE_COMPARE(out.str(), + "Trade::BasisImageConverter::convertToData(): there can be only 1 levels with base image size Vector(1, 1) but got 2\n"); +} + +void BasisImageConverterTest::levelWrongSize() { + Containers::Pointer converter = + _converterManager.instantiate("BasisImageConverter"); + + const UnsignedByte bytes[16]{}; + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!converter->convertToData({ + ImageView2D{PixelFormat::RGB8Unorm, {2, 2}, bytes}, + ImageView2D{PixelFormat::RGB8Unorm, {2, 1}, bytes} + })); + CORRADE_COMPARE(out.str(), + "Trade::BasisImageConverter::convertToData(): expected size Vector(1, 1) for level 1 but got Vector(2, 1)\n"); +} + void BasisImageConverterTest::processError() { Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); converter->configuration().setValue("max_endpoint_clusters", 16128 /* basisu_frontend::cMaxEndpointClusters */ + 1); - Image2D imageWithSkip{PixelFormat::RGBA8Unorm, Vector2i{16}, - Containers::Array{ValueInit, 16*16*4}}; + const char bytes[4]{}; + ImageView2D image{PixelFormat::RGBA8Unorm, Vector2i{1}, bytes}; std::ostringstream out; Error redirectError{&out}; - CORRADE_VERIFY(!converter->convertToData(imageWithSkip)); + CORRADE_VERIFY(!converter->convertToData(image)); CORRADE_COMPARE(out.str(), "Trade::BasisImageConverter::convertToData(): frontend processing failed\n"); } +void BasisImageConverterTest::configPerceptual() { + const char bytes[4]{}; + ImageView2D originalImage{PixelFormat::RGBA8Unorm, Vector2i{1}, bytes}; + + Containers::Pointer converter = + _converterManager.instantiate("BasisImageConverter"); + /* Empty by default */ + CORRADE_COMPARE(converter->configuration().value("perceptual"), ""); + + const auto compressedDataAutomatic = converter->convertToData(originalImage); + CORRADE_VERIFY(compressedDataAutomatic); + + converter->configuration().setValue("perceptual", true); + + const auto compressedDataOverridden = converter->convertToData(originalImage); + CORRADE_VERIFY(compressedDataOverridden); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + + /* Empty perceptual config means to use the image format to determine if + the output data should be sRGB */ + CORRADE_VERIFY(importer->openData(compressedDataAutomatic)); + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + + /* Perceptual true/false overrides the input format and forces sRGB on/off */ + CORRADE_VERIFY(importer->openData(compressedDataOverridden)); + image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); +} + +void BasisImageConverterTest::configMipGen() { + const char bytes[16*16*4]{}; + ImageView2D originalLevel0{PixelFormat::RGBA8Unorm, Vector2i{16}, bytes}; + ImageView2D originalLevel1{PixelFormat::RGBA8Unorm, Vector2i{8}, bytes}; + + Containers::Pointer converter = + _converterManager.instantiate("BasisImageConverter"); + /* Empty by default */ + CORRADE_COMPARE(converter->configuration().value("mip_gen"), false); + converter->configuration().setValue("mip_gen", ""); + + const auto compressedDataGenerated = converter->convertToData({originalLevel0}); + CORRADE_VERIFY(compressedDataGenerated); + + const auto compressedDataProvided = converter->convertToData({originalLevel0, originalLevel1}); + CORRADE_VERIFY(compressedDataProvided); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + + /* Empty mip_gen config means to use the level count to determine if mip + levels should be generated */ + CORRADE_VERIFY(importer->openData(compressedDataGenerated)); + CORRADE_COMPARE(importer->image2DLevelCount(0), 5); + + CORRADE_VERIFY(importer->openData(compressedDataProvided)); + CORRADE_COMPARE(importer->image2DLevelCount(0), 2); +} + +template +Image2D copyImageWithSkip(const ImageView2D& originalImage, Vector3i skip, PixelFormat format) { + const Vector2i size = originalImage.size(); + /* Width includes row alignment to 4 bytes */ + const UnsignedInt formatSize = pixelSize(format); + const UnsignedInt widthWithSkip = ((size.x() + skip.x())*DestinationType::Size + 3)/formatSize*formatSize; + const UnsignedInt dataSize = widthWithSkip*(size.y() + skip.y()); + Image2D imageWithSkip{PixelStorage{}.setSkip(skip), format, + size, Containers::Array{ValueInit, dataSize}}; + Utility::copy(Containers::arrayCast( + originalImage.pixels()), + imageWithSkip.pixels()); + return imageWithSkip; +} + void BasisImageConverterTest::r() { if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); - Containers::Pointer pngImporter = - _manager.instantiate("PngImporter"); - pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png")); + auto&& data = FormatTransferFunctionData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"))); const auto originalImage = pngImporter->image2D(0); CORRADE_VERIFY(originalImage); - /* Use the original image and add a skip of {7, 8} to ensure the converter - reads the image data properly. Data size is computed with row alignment - to 4 bytes. During copy, we only use R channel to retrieve a R8 image */ - const UnsignedInt dataSize = (63 + 7 + 2)*(27 + 8); - Image2D imageWithSkip{PixelStorage{}.setSkip({7, 8, 0}), - PixelFormat::R8Unorm, originalImage->size(), Containers::Array{ValueInit, dataSize}}; - Utility::copy(Containers::arrayCast( - originalImage->pixels()), - imageWithSkip.pixels()); + /* Use the original image and add a skip to ensure the converter reads the + image data properly. During copy, we only use R channel to retrieve an + R8 image. */ + const Image2D imageWithSkip = copyImageWithSkip>( + *originalImage, {7, 8, 0}, TransferFunctionFormats[data.transferFunction][0]); const auto compressedData = _converterManager.instantiate("BasisImageConverter")->convertToData(imageWithSkip); CORRADE_VERIFY(compressedData); @@ -162,14 +423,20 @@ void BasisImageConverterTest::r() { CORRADE_VERIFY(importer->openData(compressedData)); Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), TransferFunctionFormats[data.transferFunction][3]); + /* CompareImage doesn't support Srgb formats, so we need to create a view + on the original image, but with a Unorm format */ + const ImageView2D imageViewUnorm{imageWithSkip.storage(), + TransferFunctionFormats[TransferFunction::Linear][0], imageWithSkip.size(), imageWithSkip.data()}; /* Basis can only load RGBA8 uncompressed data, which corresponds to RRR1 from our R8 image data. We chose the red channel from the imported image - to compare to our original data */ + to compare to our original data. */ CORRADE_COMPARE_WITH( (Containers::arrayCast<2, const UnsignedByte>(image->pixels().prefix( {std::size_t(image->size()[1]), std::size_t(image->size()[0]), 1}))), - imageWithSkip, + imageViewUnorm, /* There are moderately significant compression artifacts */ (DebugTools::CompareImage{21.0f, 0.822f})); } @@ -178,22 +445,19 @@ void BasisImageConverterTest::rg() { if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); - Containers::Pointer pngImporter = - _manager.instantiate("PngImporter"); - pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png")); + auto&& data = FormatTransferFunctionData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"))); const auto originalImage = pngImporter->image2D(0); CORRADE_VERIFY(originalImage); - /* Use the original image and add a skip of {7, 8} to ensure the converter - reads the image data properly. Data size is computed with row alignment - to 4 bytes. During copy, we only use R and G channels to retrieve a RG8 - image. */ - const UnsignedInt dataSize = ((63 + 8)*2 + 2)*(27 + 7); - Image2D imageWithSkip{PixelStorage{}.setSkip({7, 8, 0}), - PixelFormat::RG8Unorm, originalImage->size(), Containers::Array{ValueInit, dataSize}}; - Utility::copy(Containers::arrayCast( - originalImage->pixels()), - imageWithSkip.pixels()); + /* Use the original image and add a skip to ensure the converter reads the + image data properly. During copy, we only use R and G channels to + retrieve an RG8 image. */ + const Image2D imageWithSkip = copyImageWithSkip( + *originalImage, {7, 8, 0}, TransferFunctionFormats[data.transferFunction][1]); const auto compressedData = _converterManager.instantiate("BasisImageConverter")->convertToData(imageWithSkip); CORRADE_VERIFY(compressedData); @@ -206,13 +470,19 @@ void BasisImageConverterTest::rg() { CORRADE_VERIFY(importer->openData(compressedData)); Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), TransferFunctionFormats[data.transferFunction][3]); + /* CompareImage doesn't support Srgb formats, so we need to create a view + on the original image, but with a Unorm format */ + const ImageView2D imageViewUnorm{imageWithSkip.storage(), + TransferFunctionFormats[TransferFunction::Linear][1], imageWithSkip.size(), imageWithSkip.data()}; /* Basis can only load RGBA8 uncompressed data, which corresponds to RRRG from our RG8 image data. We chose the B and A channels from the imported image to compare to our original data */ CORRADE_COMPARE_WITH( (Containers::arrayCast<2, const Math::Vector2>(image->pixels().suffix({0, 0, 2}))), - imageWithSkip, + imageViewUnorm, /* There are moderately significant compression artifacts */ (DebugTools::CompareImage{22.0f, 0.775f})); } @@ -221,21 +491,18 @@ void BasisImageConverterTest::rgb() { if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); - Containers::Pointer pngImporter = - _manager.instantiate("PngImporter"); - pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png")); + auto&& data = FormatTransferFunctionData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"))); const auto originalImage = pngImporter->image2D(0); CORRADE_VERIFY(originalImage); - /* Use the original image and add a skip of {7, 8} to ensure the converter - reads the image data properly. Data size is computed with row alignment - to 4 bytes. */ - const UnsignedInt dataSize = ((63 + 7)*3 + 3)*(27 + 8); - Image2D imageWithSkip{PixelStorage{}.setSkip({7, 8, 0}), - PixelFormat::RGB8Unorm, originalImage->size(), - Containers::Array{ValueInit, dataSize}}; - Utility::copy(originalImage->pixels(), - imageWithSkip.pixels()); + /* Use the original image and add a skip to ensure the converter reads the + image data properly */ + const Image2D imageWithSkip = copyImageWithSkip( + *originalImage, {7, 8, 0}, TransferFunctionFormats[data.transferFunction][2]); const auto compressedData = _converterManager.instantiate("BasisImageConverter")->convertToData(imageWithSkip); CORRADE_VERIFY(compressedData); @@ -248,37 +515,128 @@ void BasisImageConverterTest::rgb() { CORRADE_VERIFY(importer->openData(compressedData)); Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), TransferFunctionFormats[data.transferFunction][3]); CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels()), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 55.7f, 6.589f})); + (DebugTools::CompareImageToFile{_manager, 61.0f, 6.588f})); } void BasisImageConverterTest::rgba() { if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + auto&& data = FormatTransferFunctionData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"))); + const auto originalImage = pngImporter->image2D(0); + CORRADE_VERIFY(originalImage); + + /* Use the original image and add a skip to ensure the converter reads the + image data properly */ + const Image2D imageWithSkip = copyImageWithSkip( + *originalImage, {7, 8, 0}, TransferFunctionFormats[data.transferFunction][3]); + + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); + const auto compressedData = converter->convertToData(imageWithSkip); + CORRADE_VERIFY(compressedData); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openData(compressedData)); + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), TransferFunctionFormats[data.transferFunction][3]); + + /* Basis can only load RGBA8 uncompressed data, which corresponds to RGB1 + from our RGB8 image data. */ + CORRADE_COMPARE_WITH(image->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 97.25f, 8.547f})); +} + +void BasisImageConverterTest::convertToFile() { + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + auto&& data = ConvertToFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"))); + const auto originalLevel0 = pngImporter->image2D(0); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"))); + const auto originalLevel1 = pngImporter->image2D(0); + CORRADE_VERIFY(originalLevel0); + CORRADE_VERIFY(originalLevel1); + + const ImageView2D originalLevels[2]{*originalLevel0, *originalLevel1}; + + Containers::Pointer converter = _converterManager.instantiate(data.pluginName); + std::string filename = Utility::Directory::join(BASISIMAGECONVERTER_TEST_OUTPUT_DIR, data.filename); + CORRADE_VERIFY(converter->convertToFile(originalLevels, filename)); + + /* Verify it's actually the right format */ + /** @todo use TestSuite::Compare::StringHasPrefix once it exists */ + CORRADE_VERIFY(Containers::StringView{Containers::ArrayView(Utility::Directory::read(filename))}.hasPrefix(data.prefix)); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(filename)); + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 2); + Containers::Optional level0 = importer->image2D(0, 0); + Containers::Optional level1 = importer->image2D(0, 1); + CORRADE_VERIFY(level0); + CORRADE_VERIFY(level1); + CORRADE_COMPARE_WITH(level0->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 97.25f, 7.914f})); + CORRADE_COMPARE_WITH(level1->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 81.0f, 14.302f})); + + /* The format should get reset again after so convertToData() isn't left + with some random format after */ + if(data.pluginName == "BasisImageConverter"_s) { + const auto compressedData = converter->convertToData(originalLevels); + CORRADE_VERIFY(compressedData); + CORRADE_VERIFY(Containers::StringView{Containers::arrayView(compressedData)}.hasPrefix(BasisPrefix)); + } +} + +void BasisImageConverterTest::threads() { + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + auto&& data = ThreadsData[testCaseInstanceId()]; setTestCaseDescription(data.name); - Containers::Pointer pngImporter = - _manager.instantiate("PngImporter"); - pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png")); + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"))); const auto originalImage = pngImporter->image2D(0); CORRADE_VERIFY(originalImage); - /* Use the original image and add a skip of {7, 8} to ensure the converter - reads the image data properly. */ - const UnsignedInt dataSize = ((63 + 7)*4)*(27 + 7); - Image2D imageWithSkip{PixelStorage{}.setSkip({7, 8, 0}), - PixelFormat::RGBA8Unorm, originalImage->size(), - Containers::Array{ValueInit, dataSize}}; - Utility::copy(originalImage->pixels(), - imageWithSkip.pixels()); + /* Use the original image and add a skip to ensure the converter reads the + image data properly */ + const Image2D imageWithSkip = copyImageWithSkip( + *originalImage, {7, 8, 0}, PixelFormat::RGBA8Unorm); Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); - if(data.threads) converter->configuration().setValue("threads", data.threads); + converter->configuration().setValue("threads", data.threads); const auto compressedData = converter->convertToData(imageWithSkip); CORRADE_VERIFY(compressedData); @@ -296,7 +654,162 @@ void BasisImageConverterTest::rgba() { CORRADE_COMPARE_WITH(image->pixels(), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 78.3f, 8.302f})); + (DebugTools::CompareImageToFile{_manager, 97.25f, 7.914f})); +} + +void BasisImageConverterTest::ktx() { + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + auto&& data = FlippedData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"))); + const auto originalImage = pngImporter->image2D(0); + CORRADE_VERIFY(originalImage); + + /* Use the original image and add a skip to ensure the converter reads the + image data properly */ + const Image2D imageWithSkip = copyImageWithSkip( + *originalImage, {7, 8, 0}, PixelFormat::RGBA8Unorm); + + Containers::Pointer converter = _converterManager.instantiate("BasisKtxImageConverter"); + converter->configuration().setValue("create_ktx2_file", true); + converter->configuration().setValue("y_flip", data.yFlip); + const auto compressedData = converter->convertToData(imageWithSkip); + CORRADE_VERIFY(compressedData); + const Containers::StringView compressedView{Containers::arrayView(compressedData)}; + + CORRADE_VERIFY(compressedView.hasPrefix(KtxPrefix)); + + char KTXorientation[] = "KTXorientation\0r?"; + KTXorientation[sizeof(KTXorientation) - 1] = data.yFlip ? 'u' : 'd'; + CORRADE_VERIFY(compressedView.contains(KTXorientation)); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openData(compressedData)); + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + + /* Basis can only load RGBA8 uncompressed data, which corresponds to RGB1 + from our RGB8 image data. */ + auto pixels = image->pixels(); + if(!data.yFlip) pixels = pixels.flipped<0>(); + CORRADE_COMPARE_WITH(pixels, + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 97.25f, 9.398f})); +} + +void BasisImageConverterTest::customLevels() { + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + Containers::Pointer pngImporter = _manager.instantiate("PngImporter"); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"))); + const auto originalLevel0 = pngImporter->image2D(0); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"))); + const auto originalLevel1 = pngImporter->image2D(0); + CORRADE_VERIFY(pngImporter->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"))); + const auto originalLevel2 = pngImporter->image2D(0); + CORRADE_VERIFY(originalLevel0); + CORRADE_VERIFY(originalLevel1); + CORRADE_VERIFY(originalLevel2); + + /* Use the original images and add a skip to ensure the converter reads the + image data properly */ + const Image2D level0WithSkip = copyImageWithSkip( + *originalLevel0, {7, 8, 0}, PixelFormat::RGBA8Unorm); + const Image2D level1WithSkip = copyImageWithSkip( + *originalLevel1, {7, 8, 0}, PixelFormat::RGBA8Unorm); + const Image2D level2WithSkip = copyImageWithSkip( + *originalLevel2, {7, 8, 0}, PixelFormat::RGBA8Unorm); + + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); + + /* Off by default */ + CORRADE_COMPARE(converter->configuration().value("mip_gen"), false); + /* Making sure that providing custom levels turns off automatic mip level + generation. We only provide an incomplete mip chain so we can tell if + basis generated any extra levels beyond that. */ + converter->configuration().setValue("mip_gen", true); + + std::ostringstream out; + Warning redirectWarning{&out}; + + const auto compressedData = converter->convertToData({level0WithSkip, level1WithSkip, level2WithSkip}); + CORRADE_VERIFY(compressedData); + CORRADE_COMPARE(out.str(), "Trade::BasisImageConverter::convertToData(): found user-supplied mip levels, ignoring mip_gen config value\n"); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openData(compressedData)); + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 3); + + Containers::Optional level0 = importer->image2D(0, 0); + Containers::Optional level1 = importer->image2D(0, 1); + Containers::Optional level2 = importer->image2D(0, 2); + CORRADE_VERIFY(level0); + CORRADE_VERIFY(level1); + CORRADE_VERIFY(level2); + + CORRADE_COMPARE(level0->size(), (Vector2i{63, 27})); + CORRADE_COMPARE(level1->size(), (Vector2i{31, 13})); + CORRADE_COMPARE(level2->size(), (Vector2i{15, 6})); + + /* Basis can only load RGBA8 uncompressed data, which corresponds to RGB1 + from our RGB8 image data. */ + CORRADE_COMPARE_WITH(level0->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 97.25f, 7.927f})); + CORRADE_COMPARE_WITH(level1->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 81.0f, 14.322f})); + CORRADE_COMPARE_WITH(level2->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 76.25f, 24.5f})); +} + +void BasisImageConverterTest::swizzle() { + auto&& data = SwizzleData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); + /* Default is empty */ + CORRADE_COMPARE(converter->configuration().value("swizzle"), ""); + converter->configuration().setValue("swizzle", data.swizzle); + + const Color4ub pixel[1]{data.input}; + const ImageView2D originalImage{data.format, {1, 1}, Containers::arrayCast(pixel)}; + + const auto compressedData = converter->convertToData(originalImage); + CORRADE_VERIFY(compressedData); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = + _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openData(compressedData)); + CORRADE_COMPARE(importer->image2DCount(), 1); + + const auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->size(), (Vector2i{1, 1})); + /* There are very minor compression artifacts */ + CORRADE_COMPARE_WITH(Color4{image->pixels()[0][0]}, Color4{data.output}, (TestSuite::Compare::Around{Vector4{2.0f}})); } }}}} diff --git a/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt b/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt index d221fff58..84fcd0d92 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt @@ -33,6 +33,12 @@ find_package(Magnum COMPONENTS AnyImageImporter) # pthread, the app has to be instead find_package(Threads REQUIRED) +if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(BASISIMAGECONVERTER_TEST_OUTPUT_DIR "write") +else() + set(BASISIMAGECONVERTER_TEST_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) +endif() + if(WITH_BASISIMPORTER) if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) set(BASISIMPORTER_TEST_DIR ".") diff --git a/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake b/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake index 9e6a76abb..0aff7ab2c 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake +++ b/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake @@ -28,3 +28,4 @@ #cmakedefine BASISIMPORTER_PLUGIN_FILENAME "${BASISIMPORTER_PLUGIN_FILENAME}" #cmakedefine STBIMAGEIMPORTER_PLUGIN_FILENAME "${STBIMAGEIMPORTER_PLUGIN_FILENAME}" #cmakedefine BASISIMPORTER_TEST_DIR "${BASISIMPORTER_TEST_DIR}" +#define BASISIMAGECONVERTER_TEST_OUTPUT_DIR "$BASISIMAGECONVERTER_TEST_OUTPUT_DIR}" diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.conf b/src/MagnumPlugins/BasisImporter/BasisImporter.conf index a7865a7e0..1e11ee403 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.conf +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.conf @@ -6,7 +6,6 @@ provides=BasisImporterBc1RGB provides=BasisImporterBc3RGBA provides=BasisImporterBc4R provides=BasisImporterBc5RG -provides=BasisImporterBc7RGB provides=BasisImporterBc7RGBA provides=BasisImporterPvrtcRGB4bpp provides=BasisImporterPvrtcRGBA4bpp diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp index 6744b6aad..dcac09d2d 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp @@ -4,6 +4,7 @@ Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Vladimír Vondruš Copyright © 2019, 2021 Jonathan Hale + Copyright © 2021 Pablo Escobar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -26,52 +27,58 @@ #include "BasisImporter.h" -#include - +#include #include -#include #include #include #include #include +#include #include #include #include +#include + +#include namespace Magnum { namespace Trade { namespace { -/* Map BasisImporter::TargetFormat to CompressedPixelFormat. See the +/* Map BasisImporter::TargetFormat to (Compressed)PixelFormat. See the TargetFormat enum for details. */ -CompressedPixelFormat compressedPixelFormat(BasisImporter::TargetFormat type) { +PixelFormat pixelFormat(BasisImporter::TargetFormat type, bool isSrgb) { + switch(type) { + case BasisImporter::TargetFormat::RGBA8: + return isSrgb ? PixelFormat::RGBA8Srgb : PixelFormat::RGBA8Unorm; + default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } +} + +CompressedPixelFormat compressedPixelFormat(BasisImporter::TargetFormat type, bool isSrgb) { switch(type) { - /** @todo sRGB once https://github.com/BinomialLLC/basis_universal/issues/66 - is fixed */ case BasisImporter::TargetFormat::Etc1RGB: - return CompressedPixelFormat::Etc2RGB8Unorm; + return isSrgb ? CompressedPixelFormat::Etc2RGB8Srgb : CompressedPixelFormat::Etc2RGB8Unorm; case BasisImporter::TargetFormat::Etc2RGBA: - return CompressedPixelFormat::Etc2RGBA8Unorm; + return isSrgb ? CompressedPixelFormat::Etc2RGBA8Srgb : CompressedPixelFormat::Etc2RGBA8Unorm; case BasisImporter::TargetFormat::Bc1RGB: - return CompressedPixelFormat::Bc1RGBUnorm; + return isSrgb ? CompressedPixelFormat::Bc1RGBSrgb : CompressedPixelFormat::Bc1RGBUnorm; case BasisImporter::TargetFormat::Bc3RGBA: - return CompressedPixelFormat::Bc3RGBAUnorm; - /** @todo use bc7/bc4/bc5 based on channel count? needs a bit from the - above issue as well */ + return isSrgb ? CompressedPixelFormat::Bc3RGBASrgb : CompressedPixelFormat::Bc3RGBAUnorm; + /** @todo use bc7/bc4/bc5 based on channel count? needs a bit from + https://github.com/BinomialLLC/basis_universal/issues/66 */ case BasisImporter::TargetFormat::Bc4R: return CompressedPixelFormat::Bc4RUnorm; case BasisImporter::TargetFormat::Bc5RG: return CompressedPixelFormat::Bc5RGUnorm; - case BasisImporter::TargetFormat::Bc7RGB: - return CompressedPixelFormat::Bc7RGBAUnorm; case BasisImporter::TargetFormat::Bc7RGBA: - return CompressedPixelFormat::Bc7RGBAUnorm; + return isSrgb ? CompressedPixelFormat::Bc7RGBASrgb : CompressedPixelFormat::Bc7RGBAUnorm; case BasisImporter::TargetFormat::PvrtcRGB4bpp: - return CompressedPixelFormat::PvrtcRGB4bppUnorm; + return isSrgb ? CompressedPixelFormat::PvrtcRGB4bppSrgb : CompressedPixelFormat::PvrtcRGB4bppUnorm; case BasisImporter::TargetFormat::PvrtcRGBA4bpp: - return CompressedPixelFormat::PvrtcRGBA4bppUnorm; + return isSrgb ? CompressedPixelFormat::PvrtcRGBA4bppSrgb : CompressedPixelFormat::PvrtcRGBA4bppUnorm; case BasisImporter::TargetFormat::Astc4x4RGBA: - return CompressedPixelFormat::Astc4x4RGBAUnorm; + return isSrgb ? CompressedPixelFormat::Astc4x4RGBASrgb : CompressedPixelFormat::Astc4x4RGBAUnorm; /** @todo use etc2/eacR/eacRG based on channel count? needs a bit from - the above issue as well */ + https://github.com/BinomialLLC/basis_universal/issues/66 */ case BasisImporter::TargetFormat::EacR: return CompressedPixelFormat::EacR11Unorm; case BasisImporter::TargetFormat::EacRG: @@ -82,7 +89,7 @@ CompressedPixelFormat compressedPixelFormat(BasisImporter::TargetFormat type) { constexpr const char* FormatNames[]{ "Etc1RGB", "Etc2RGBA", - "Bc1RGB", "Bc3RGBA", "Bc4R", "Bc5RG", "Bc7RGB", "Bc7RGBA", + "Bc1RGB", "Bc3RGBA", "Bc4R", "Bc5RG", "Bc7RGBA", nullptr, /* BC7_ALT */ "PvrtcRGB4bpp", "PvrtcRGBA4bpp", "Astc4x4RGBA", nullptr, nullptr, /* ATC formats */ @@ -125,11 +132,36 @@ namespace Magnum { namespace Trade { struct BasisImporter::State { /* There is only this type of codebook */ basist::etc1_global_selector_codebook codebook; - Containers::Optional transcoder; + + /* One transcoder for each supported file type, and of course they have + wildly different interfaces. ktx2_transcoder is only defined if + BASISD_SUPPORT_KTX2 != 0, so we need to ifdef around it everywhere + it's used. */ + Containers::Optional basisTranscoder; + #if BASISD_SUPPORT_KTX2 + Containers::Optional ktx2Transcoder; + #else + Containers::Optional ktx2Transcoder; + #endif + Containers::Array in; - basist::basisu_file_info fileInfo; + + TextureType textureType; + /* Only > 1 for plain 2D .basis files with multiple images. Anything else + (cube/array/volume) contains a single logical image. KTX2 only supports + one image to begin with. */ + UnsignedInt numImages; + /* Number of faces/layers/z-slices in the third image dimension, 1 if 2D */ + UnsignedInt numSlices; + Containers::Array numLevels; + + basist::basis_tex_format compressionType; + bool isVideo; + bool isYFlipped; + bool isSrgb; bool noTranscodeFormatWarningPrinted = false; + UnsignedInt lastTranscodedImageId = ~0u; explicit State(): codebook(basist::g_global_selector_cb_size, basist::g_global_selector_cb) {} @@ -162,14 +194,15 @@ BasisImporter::~BasisImporter() = default; ImporterFeatures BasisImporter::doFeatures() const { return ImporterFeature::OpenData; } bool BasisImporter::doIsOpened() const { - /* Both the transcoder and then input data have to be present or both + /* Both a transcoder and the input data have to be present or both have to be empty */ - CORRADE_INTERNAL_ASSERT(!_state->transcoder == !_state->in); + CORRADE_INTERNAL_ASSERT(!(_state->basisTranscoder || _state->ktx2Transcoder) == !_state->in); return _state->in; } void BasisImporter::doClose() { - _state->transcoder = Containers::NullOpt; + _state->basisTranscoder = Containers::NullOpt; + _state->ktx2Transcoder = Containers::NullOpt; _state->in = nullptr; } @@ -186,106 +219,423 @@ void BasisImporter::doOpenData(Containers::Array&& data, DataFlags dataFla return; } - _state->transcoder.emplace(&_state->codebook); - Containers::ScopeGuard transcoderGuard{&_state->transcoder, [](Containers::Optional* o) { - *o = Containers::NullOpt; - }}; - if(!_state->transcoder->validate_header(data.data(), data.size())) { - Error() << "Trade::BasisImporter::openData(): invalid header"; - return; + /* Check if this is a KTX2 file. There's basist::g_ktx2_file_identifier but + that's hidden behind BASISD_SUPPORT_KTX2 so define it ourselves, taken + from KtxImporter/KtxHeader.h */ + constexpr char KtxFileIdentifier[12]{'\xab', 'K', 'T', 'X', ' ', '2', '0', '\xbb', '\r', '\n', '\x1a', '\n'}; + const bool isKTX2 = data.size() >= sizeof(KtxFileIdentifier) && + std::memcmp(data.begin(), KtxFileIdentifier, sizeof(KtxFileIdentifier)) == 0; + + /* Keep the original data, take over the existing array or copy the data if + we can't. We have to do this first because transcoders may hold on to + the pointer we pass into init()/start_transcoding(). While + basis_transcoder currently doesn't keep the pointer around, it might in + the future and ktx2_transcoder already does. */ + Containers::Pointer state{InPlaceInit}; + if(dataFlags & (DataFlag::Owned|DataFlag::ExternallyOwned)) { + state->in = std::move(data); + } else { + state->in = Containers::Array{NoInit, data.size()}; + Utility::copy(data, state->in); } - /* Save the global file info to avoid calling that again each time we check - for image count and whatnot; start transcoding */ - if(!_state->transcoder->get_file_info(data.data(), data.size(), _state->fileInfo) || - !_state->transcoder->start_transcoding(data.data(), data.size())) { - Error() << "Trade::BasisImporter::openData(): bad basis file"; + if(isKTX2) { + #if !BASISD_SUPPORT_KTX2 + /** @todo Can we test this? Maybe disable this on some CI, BC7 is + already disabled on Emscripten. */ + Error{} << "Trade::BasisImporter::openData(): opening a KTX2 file but Basis Universal was compiled without KTX2 support"; return; - } + #else + state->ktx2Transcoder.emplace(&state->codebook); + + /* init() handles all the validation checks, there's no extra function + for that */ + if(!state->ktx2Transcoder->init(state->in.data(), state->in.size())) { + Error{} << "Trade::BasisImporter::openData(): invalid KTX2 header, or not Basis compressed"; + return; + } + + /* Check for supercompression and print a useful error if basisu was + compiled without Zstandard support. Not exposed in ktx2_transcoder, + get it from the KTX2 header directly. */ + /** @todo Can we test this? Maybe disable this on some CI, BC7 is + already disabled on Emscripten. */ + const basist::ktx2_header& header = *reinterpret_cast(state->in.data()); + if(header.m_supercompression_scheme == basist::KTX2_SS_ZSTANDARD && !BASISD_SUPPORT_KTX2_ZSTD) { + Error{} << "Trade::BasisImporter::openData(): file uses Zstandard supercompression but Basis Universal was compiled without Zstandard support"; + return; + } + + /* Start transcoding */ + if(!state->ktx2Transcoder->start_transcoding()) { + Error{} << "Trade::BasisImporter::openData(): bad KTX2 file"; + return; + } + + /* Save some global file info we need later */ + + /* Remember the type for doTexture(). ktx2_transcoder::init() already + checked we're dealing with a valid 2D texture. basisu -tex_type 3d + results in 2D array textures, and there's no get_depth() at all. */ + state->isVideo = false; + if(state->ktx2Transcoder->get_faces() != 1) + state->textureType = state->ktx2Transcoder->get_layers() > 0 ? TextureType::CubeMapArray : TextureType::CubeMap; + else if(state->ktx2Transcoder->is_video()) { + state->textureType = TextureType::Texture2D; + state->isVideo = true; + } else + state->textureType = state->ktx2Transcoder->get_layers() > 0 ? TextureType::Texture2DArray : TextureType::Texture2D; + + /* KTX2 files only ever contain one image, but for videos we choose to + expose layers as multiple images, one for each frame */ + if(state->isVideo) { + state->numImages = Math::max(state->ktx2Transcoder->get_layers(), 1u); + state->numSlices = 1; + } else { + state->numImages = 1; + state->numSlices = state->ktx2Transcoder->get_faces()*Math::max(state->ktx2Transcoder->get_layers(), 1u); + } + state->numLevels = Containers::Array{DirectInit, state->numImages, state->ktx2Transcoder->get_levels()}; + + state->compressionType = state->ktx2Transcoder->get_format(); + + /* Get y-flip flag from KTXorientation key/value entry. If it's + missing, the default is Y-down. Y-up = flipped. */ + const basisu::uint8_vec* orientation = state->ktx2Transcoder->find_key("KTXorientation"); + state->isYFlipped = orientation && orientation->size() >= 2 && (*orientation)[1] == 'u'; + + state->isSrgb = state->ktx2Transcoder->get_dfd_transfer_func() == basist::KTX2_KHR_DF_TRANSFER_SRGB; + #endif - /* All good, release the transcoder guard. Keep the original data, take - over the existing array or copy the data if we can't. */ - transcoderGuard.release(); - if(dataFlags & (DataFlag::Owned|DataFlag::ExternallyOwned)) { - _state->in = std::move(data); } else { - _state->in = Containers::Array{NoInit, data.size()}; - Utility::copy(data, _state->in); + /* .basis file */ + state->basisTranscoder.emplace(&state->codebook); + + if(!state->basisTranscoder->validate_header(state->in.data(), state->in.size())) { + Error{} << "Trade::BasisImporter::openData(): invalid basis header"; + return; + } + + /* Start transcoding */ + basist::basisu_file_info fileInfo; + if(!state->basisTranscoder->get_file_info(state->in.data(), state->in.size(), fileInfo) || + !state->basisTranscoder->start_transcoding(state->in.data(), state->in.size())) { + Error{} << "Trade::BasisImporter::openData(): bad basis file"; + return; + } + + /* Save some global file info we need later. We can't save fileInfo + directly because that's specific to .basis files, and there's no + equivalent in ktx2_transcoder. */ + + /* This is checked by basis_transcoder::start_transcoding() */ + CORRADE_INTERNAL_ASSERT(fileInfo.m_total_images > 0); + + /* Remember the type for doTexture(). Depending on the type, we're + either dealing with multiple independent 2D images or each image is + an array layer, cubemap face or depth slice. */ + state->isVideo = false; + switch(fileInfo.m_tex_type) { + case basist::basis_texture_type::cBASISTexTypeVideoFrames: + /* Decoding all video frames at once is usually not what you + want, so we treat videos as independent 2D images. We still + have to check that the sizes match, and need to remember + this is a video to disallow seeking (not supported by + basisu). */ + state->isVideo = true; + state->textureType = TextureType::Texture2D; + break; + case basist::basis_texture_type::cBASISTexType2D: + state->textureType = TextureType::Texture2D; + break; + case basist::basis_texture_type::cBASISTexType2DArray: + state->textureType = TextureType::Texture2DArray; + break; + case basist::basis_texture_type::cBASISTexTypeCubemapArray: + state->textureType = fileInfo.m_total_images > 6 ? TextureType::CubeMapArray : TextureType::CubeMap; + break; + case basist::basis_texture_type::cBASISTexTypeVolume: + /* Import 3D textures as 2D arrays because: + - 3D textures are not supported in KTX2 so this unifies the + formats + - mip levels are always 2D images for each slice, meaning + they wouldn't halve in the z-dimension as users would very + likely expect */ + Warning{} << "Trade::BasisImporter::openData(): importing 3D texture as a 2D array texture"; + state->textureType = TextureType::Texture2DArray; + break; + default: + /* This is caught by basis_transcoder::get_file_info() */ + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + } + + if(state->textureType == TextureType::Texture2D) { + state->numImages = fileInfo.m_total_images; + state->numSlices = 1; + } else { + state->numImages = 1; + state->numSlices = fileInfo.m_total_images; + } + + CORRADE_INTERNAL_ASSERT(fileInfo.m_image_mipmap_levels.size() == fileInfo.m_total_images); + + /* Get mip level counts and check that they, as well as resolution, are + equal for all non-independent images (layers, faces, video frames). + These checks, including the cube map checks below, are either not + necessary for the KTX2 file format or are already handled by + ktx2_transcoder. */ + const bool imageSizeMustMatch = state->textureType != TextureType::Texture2D || state->isVideo; + UnsignedInt firstWidth = 0, firstHeight = 0; + state->numLevels = Containers::Array{NoInit, state->numImages}; + for(UnsignedInt i = 0; i != fileInfo.m_total_images; ++i) { + /* Header validation etc. is already done in get_file_info and + start_transcoding, so by looking at the code there's nothing else + that could fail and wasn't already caught before */ + basist::basisu_image_info imageInfo; + CORRADE_INTERNAL_ASSERT_OUTPUT(state->basisTranscoder->get_image_info(state->in.data(), state->in.size(), imageInfo, i)); + if(i == 0) { + firstWidth = imageInfo.m_orig_width; + firstHeight = imageInfo.m_orig_height; + } + + if(imageSizeMustMatch && (imageInfo.m_orig_width != firstWidth || imageInfo.m_orig_height != firstHeight)) { + Error{} << "Trade::BasisImporter::openData(): image slices have mismatching size, expected" + << firstWidth << "by" << firstHeight << "but got" + << imageInfo.m_orig_width << "by" << imageInfo.m_orig_height; + return; + } + + if(imageSizeMustMatch && i > 0 && imageInfo.m_total_levels != state->numLevels[0]) { + Error{} << "Trade::BasisImporter::openData(): image slices have mismatching level counts, expected" + << state->numLevels[0] << "but got" << imageInfo.m_total_levels; + return; + } + + if(i < state->numImages) + state->numLevels[i] = imageInfo.m_total_levels; + } + + if(state->textureType == TextureType::CubeMap || state->textureType == TextureType::CubeMapArray) { + if(state->numSlices % 6 != 0) { + Error{} << "Trade::BasisImporter::openData(): cube map face count must be a multiple of 6 but got" << state->numSlices; + return; + } + + if(firstWidth != firstHeight) { + Error{} << "Trade::BasisImporter::openData(): cube map must be square but got" + << firstWidth << "by" << firstHeight; + return; + } + } + + state->compressionType = fileInfo.m_tex_format; + state->isYFlipped = fileInfo.m_y_flipped; + + /* For some reason cBASISHeaderFlagSRGB is not exposed in basisu_file_info, + get it from the basis header directly */ + const basist::basis_file_header& header = *reinterpret_cast(state->in.data()); + state->isSrgb = header.m_flags & basist::basis_header_flags::cBASISHeaderFlagSRGB; } -} -UnsignedInt BasisImporter::doImage2DCount() const { - return _state->fileInfo.m_total_images; -} + /* There has to be exactly one transcoder */ + CORRADE_INTERNAL_ASSERT(!state->ktx2Transcoder != !state->basisTranscoder); + /* Can't have a KTX2 transcoder without KTX2 support compiled into basisu */ + CORRADE_INTERNAL_ASSERT(BASISD_SUPPORT_KTX2 || !state->ktx2Transcoder); + /* These file formats don't support 1D images and we import 3D images as + 2D array images */ + CORRADE_INTERNAL_ASSERT(state->textureType != TextureType::Texture1D && + state->textureType != TextureType::Texture1DArray && + state->textureType != TextureType::Texture3D); + /* There's one image with faces/layers, or multiple images without any */ + CORRADE_INTERNAL_ASSERT(state->numImages == 1 || state->numSlices == 1); + + if(flags() & ImporterFlag::Verbose) { + if(state->isVideo) + Debug{} << "Trade::BasisImporter::openData(): file contains video frames, images must be transcoded sequentially"; + } -UnsignedInt BasisImporter::doImage2DLevelCount(const UnsignedInt id) { - return _state->fileInfo.m_image_mipmap_levels[id]; + _state = std::move(state); } -Containers::Optional BasisImporter::doImage2D(const UnsignedInt id, const UnsignedInt level) { - std::string targetFormatStr = configuration().value("format"); +template +Containers::Optional> BasisImporter::doImage(const UnsignedInt id, const UnsignedInt level) { + static_assert(dimensions >= 2 && dimensions <= 3, "Only 2D and 3D images are supported"); + constexpr const char* prefixes[2]{"Trade::BasisImporter::image2D():", "Trade::BasisImporter::image3D():"}; + constexpr const char* prefix = prefixes[dimensions - 2]; + + const std::string targetFormatStr = configuration().value("format"); TargetFormat targetFormat; if(targetFormatStr.empty()) { if(!_state->noTranscodeFormatWarningPrinted) - Warning{} << "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning either load the plugin via one of its BasisImporterEtc1RGB, ... aliases, or explicitly set the format option in plugin configuration."; + Warning{} << prefix << "no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning either load the plugin via one of its BasisImporterEtc1RGB, ... aliases, or explicitly set the format option in plugin configuration."; targetFormat = TargetFormat::RGBA8; _state->noTranscodeFormatWarningPrinted = true; } else { targetFormat = configuration().value("format"); if(UnsignedInt(targetFormat) == ~UnsignedInt{}) { - Error() << "Trade::BasisImporter::image2D(): invalid transcoding target format" - << targetFormatStr.data() << Debug::nospace << ", expected to be one of EacR, EacRG, Etc1RGB, Etc2RGBA, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, RGBA8"; + Error{} << prefix << "invalid transcoding target format" << targetFormatStr << Debug::nospace + << ", expected to be one of EacR, EacRG, Etc1RGB, Etc2RGBA, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, RGBA8"; return Containers::NullOpt; } } + const auto format = basist::transcoder_texture_format(Int(targetFormat)); + const bool isUncompressed = basist::basis_transcoder_format_is_uncompressed(format); + + /* Some target formats may be unsupported, either because support wasn't + compiled in or UASTC doesn't support a certain format. All of the + formats in TargetFormat are transcodable from UASTC so we can provide a + slightly more useful error message than "impossible!". */ + if(!basist::basis_is_format_supported(format, _state->compressionType)) { + Error{} << prefix << "Basis Universal was compiled without support for" << targetFormatStr; + return Containers::NullOpt; + } - basist::basisu_image_info info; - /* Header validation etc. is already done in doOpenData() and id is - bounds-checked against doImage2DCount() by AbstractImporter, so by - looking at the code there's nothing else that could fail and wasn't - already caught before. That means we also can't craft any file to cover - an error path, so turning this into an assert. When this blows up for - someome, we'd most probably need to harden doOpenData() to catch that, - not turning this into a graceful error. */ - CORRADE_INTERNAL_ASSERT_OUTPUT(_state->transcoder->get_image_info(_state->in.data(), _state->in.size(), info, id)); - - UnsignedInt origWidth, origHeight, totalBlocks; - /* Same as above, it checks for state we already verified before. If this - blows up for someone, we can reconsider. */ - CORRADE_INTERNAL_ASSERT_OUTPUT(_state->transcoder->get_image_level_desc(_state->in.data(), _state->in.size(), id, level, origWidth, origHeight, totalBlocks)); + UnsignedInt origWidth, origHeight, totalBlocks, numFaces; + bool isIFrame; + if(_state->ktx2Transcoder) { + #if BASISD_SUPPORT_KTX2 + basist::ktx2_image_level_info levelInfo; + /* Header validation etc. is already done in doOpenData() and id is + bounds-checked against doImage2DCount() by AbstractImporter, so by + looking at the code there's nothing else that could fail and wasn't + already caught before. That means we also can't craft any file to + cover an error path, so turning this into an assert. When this blows + up for someome, we'd most probably need to harden doOpenData() to + catch that, not turning this into a graceful error. + + For independent images and videos we use the correct layer. For + images as slices, they're all the same size, (checked in + doOpenData()) and isIFrame is not used, so any layer or face works. */ + CORRADE_INTERNAL_ASSERT_OUTPUT(_state->ktx2Transcoder->get_image_level_info(levelInfo, level, id, 0)); + + origWidth = levelInfo.m_orig_width; + origHeight = levelInfo.m_orig_height; + totalBlocks = levelInfo.m_total_blocks; + /* m_iframe_flag is always false for UASTC video: + https://github.com/BinomialLLC/basis_universal/issues/259 + However, it's safe to assume the first frame is always an I-frame. */ + isIFrame = levelInfo.m_iframe_flag || id == 0; + + numFaces = _state->ktx2Transcoder->get_faces(); + #endif + } else { + /* See comment right above */ + basist::basisu_image_level_info levelInfo; + CORRADE_INTERNAL_ASSERT_OUTPUT(_state->basisTranscoder->get_image_level_info(_state->in.data(), _state->in.size(), levelInfo, id, level)); + + origWidth = levelInfo.m_orig_width; + origHeight = levelInfo.m_orig_height; + totalBlocks = levelInfo.m_total_blocks; + isIFrame = levelInfo.m_iframe_flag; + + numFaces = (_state->textureType == TextureType::CubeMap || _state->textureType == TextureType::CubeMapArray) ? 6 : 1; + } + + const UnsignedInt numLayers = _state->numSlices/numFaces; + + /* basisu doesn't allow seeking to arbitrary video frames. If this isn't an + I-frame, only allow transcoding the frame following the last P-frame. */ + if(_state->isVideo) { + const UnsignedInt expectedImageId = _state->lastTranscodedImageId + 1; + if(!isIFrame && id != expectedImageId) { + Error{} << prefix << "video frames must be transcoded sequentially, expected frame" + << expectedImageId << (expectedImageId == 0 ? "but got" : "or 0 but got") << id; + return Containers::NullOpt; + } + _state->lastTranscodedImageId = id; + } /* No flags used by transcode_image_level() by default */ const std::uint32_t flags = 0; - if(!_state->fileInfo.m_y_flipped) { + if(!_state->isYFlipped) { /** @todo replace with the flag once the PR is submitted */ - Warning{} << "Trade::BasisImporter::image2D(): the image was not encoded Y-flipped, imported data will have wrong orientation"; + Warning{} << prefix << "the image was not encoded Y-flipped, imported data will have wrong orientation"; //flags |= basist::basisu_transcoder::cDecodeFlagsFlipY; } - Vector2i size{Int(origWidth), Int(origHeight)}; - UnsignedInt dataSize, rowStride, outputSizeInBlocksOrPixels, outputRowsInPixels; - if(targetFormat == BasisImporter::TargetFormat::RGBA8) { + const Vector3ui size{origWidth, origHeight, _state->numSlices}; + UnsignedInt rowStride, outputRowsInPixels, outputSizeInBlocksOrPixels; + if(isUncompressed) { rowStride = size.x(); outputRowsInPixels = size.y(); - outputSizeInBlocksOrPixels = size.product(); - dataSize = 4*outputSizeInBlocksOrPixels; + outputSizeInBlocksOrPixels = size.x()*size.y(); } else { rowStride = 0; /* left up to Basis to calculate */ outputRowsInPixels = 0; /* not used for compressed data */ outputSizeInBlocksOrPixels = totalBlocks; - dataSize = basis_get_bytes_per_block(format)*totalBlocks; } + + const UnsignedInt sliceSize = basis_get_bytes_per_block_or_pixel(format)*outputSizeInBlocksOrPixels; + const UnsignedInt dataSize = sliceSize*size.z(); Containers::Array dest{DefaultInit, dataSize}; - if(!_state->transcoder->transcode_image_level(_state->in.data(), _state->in.size(), id, level, dest.data(), outputSizeInBlocksOrPixels, basist::transcoder_texture_format(format), flags, rowStride, nullptr, outputRowsInPixels)) { - Error{} << "Trade::BasisImporter::image2D(): transcoding failed"; - return Containers::NullOpt; + + /* There's no function for transcoding the entire level, so loop over all + layers and faces and transcode each one. This matches the image layout + imported by KtxImporter, ie. all faces +X through -Z for the first + layer, then all faces of the second layer, etc. + + If the user is requesting id > 0, there can't be any layers or faces, + this is already asserted in doOpenData(). This allows us to calculate + the layer (KTX2) or image id to transcode with a simple addition. */ + for(UnsignedInt l = 0; l != numLayers; ++l) { + for(UnsignedInt f = 0; f != numFaces; ++f) { + const UnsignedInt offset = (l*numFaces + f)*sliceSize; + if(_state->ktx2Transcoder) { + #if BASISD_SUPPORT_KTX2 + const UnsignedInt currentLayer = id + l; + if(!_state->ktx2Transcoder->transcode_image_level(level, currentLayer, f, dest.data() + offset, outputSizeInBlocksOrPixels, format, flags, rowStride, outputRowsInPixels)) { + Error{} << "Trade::BasisImporter::image2D(): transcoding failed"; + return Containers::NullOpt; + } + #endif + } else { + const UnsignedInt currentId = id + (l*numFaces + f); + if(!_state->basisTranscoder->transcode_image_level(_state->in.data(), _state->in.size(), currentId, level, dest.data() + offset, outputSizeInBlocksOrPixels, format, flags, rowStride, nullptr, outputRowsInPixels)) { + Error{} << "Trade::BasisImporter::image2D(): transcoding failed"; + return Containers::NullOpt; + } + } + } } - if(targetFormat == BasisImporter::TargetFormat::RGBA8) - return Trade::ImageData2D{PixelFormat::RGBA8Unorm, size, std::move(dest)}; + if(isUncompressed) + return Trade::ImageData{pixelFormat(targetFormat, _state->isSrgb), Math::Vector::pad(Vector3i{size}), std::move(dest)}; else - return Trade::ImageData2D{compressedPixelFormat(targetFormat), size, std::move(dest)}; + return Trade::ImageData{compressedPixelFormat(targetFormat, _state->isSrgb), Math::Vector::pad(Vector3i{size}), std::move(dest)}; +} + +UnsignedInt BasisImporter::doImage2DCount() const { + return _state->textureType == TextureType::Texture2D ? _state->numImages : 0; +} + +UnsignedInt BasisImporter::doImage2DLevelCount(const UnsignedInt id) { + return _state->numLevels[id]; +} + +Containers::Optional BasisImporter::doImage2D(const UnsignedInt id, const UnsignedInt level) { + return doImage<2>(id, level); +} + +UnsignedInt BasisImporter::doImage3DCount() const { + return _state->textureType != TextureType::Texture2D ? _state->numImages : 0; +} + +UnsignedInt BasisImporter::doImage3DLevelCount(const UnsignedInt id) { + return _state->numLevels[id]; +} + +Containers::Optional BasisImporter::doImage3D(const UnsignedInt id, const UnsignedInt level) { + return doImage<3>(id, level); +} + +UnsignedInt BasisImporter::doTextureCount() const { + return _state->numImages; +} + +Containers::Optional BasisImporter::doTexture(UnsignedInt id) { + return TextureData{_state->textureType, SamplerFilter::Linear, SamplerFilter::Linear, + SamplerMipmap::Linear, SamplerWrapping::Repeat, id}; } void BasisImporter::setTargetFormat(TargetFormat format) { diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.h b/src/MagnumPlugins/BasisImporter/BasisImporter.h index 8347ed501..78ccc7c7a 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.h +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.h @@ -6,6 +6,7 @@ Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Vladimír Vondruš Copyright © 2019, 2021 Jonathan Hale + Copyright © 2021 Pablo Escobar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -57,19 +58,21 @@ namespace Magnum { namespace Trade { @brief Basis Universal importer plugin @m_since_{plugins,2019,10} -@m_keywords{BasisImporterEacR BasisImporterEacRG BasisImporterEtc1RGB} @m_keywords{BasisImporterEtc2RGBA BasisImporterBc1RGB BasisImporterBc3RGBA} @m_keywords{BasisImporterBc4R BasisImporterBc5RG BasisImporterBc7RGB} @m_keywords{BasisImporterBc7RGBA BasisImporterPvrtc1RGB4bpp} -@m_keywords{BasisImporterPvrtc1RGBA4bpp BasisImporterAstc4x4RGBA} -@m_keywords{BasisImporterRGBA8} +@m_keywords{BasisImporterEacR BasisImporterEacRG BasisImporterEtc1RGB} +@m_keywords{BasisImporterEtc2RGBA BasisImporterBc1RGB BasisImporterBc3RGBA} +@m_keywords{BasisImporterBc4R BasisImporterBc5RG BasisImporterBc7RGBA} +@m_keywords{BasisImporterPvrtc1RGB4bpp BasisImporterPvrtc1RGBA4bpp} +@m_keywords{BasisImporterAstc4x4RGBA BasisImporterRGBA8} Supports [Basis Universal](https://github.com/binomialLLC/basis_universal) -(`*.basis`) compressed images by parsing and transcoding files into an -explicitly specified GPU format (see @ref Trade-BasisImporter-target-format). +compressed images (`*.basis` or `*.ktx2`) by parsing and transcoding files into +an explicitly specified GPU format (see @ref Trade-BasisImporter-target-format). You can use @ref BasisImageConverter to transcode images into this format. This plugin provides `BasisImporterEacR`, `BasisImporterEacRG`, `BasisImporterEtc1RGB`, `BasisImporterEtc2RGBA`, `BasisImporterBc1RGB`, `BasisImporterBc3RGBA`, `BasisImporterBc4R`, `BasisImporterBc5RG`, -`BasisImporterBc7RGB`, `BasisImporterBc7RGBA`, `BasisImporterPvrtc1RGB4bpp`, +`BasisImporterBc7RGBA`, `BasisImporterPvrtc1RGB4bpp`, `BasisImporterPvrtc1RGBA4bpp`, `BasisImporterAstc4x4RGBA`, `BasisImporterRGBA8`. @m_class{m-block m-success} @@ -86,7 +89,9 @@ This plugin provides `BasisImporterEacR`, `BasisImporterEacRG`, This plugin depends on the @ref Trade and [Basis Universal](https://github.com/binomialLLC/basis_universal) libraries and is built if `WITH_BASISIMPORTER` is enabled when building Magnum Plugins. To use as a dynamic plugin, load @cpp "BasisImporter" @ce via -@ref Corrade::PluginManager::Manager. +@ref Corrade::PluginManager::Manager. Current version of the plugin is tested +against the [`v1_15_update2` tag](https://github.com/BinomialLLC/basis_universal/tree/v1_15_update2), +but could possibly compile against newer versions as well. Additionally, if you're using Magnum as a CMake subproject, bundle the [magnum-plugins](https://github.com/mosra/magnum-plugins) and @@ -118,6 +123,64 @@ target_link_libraries(your-app PRIVATE MagnumPlugins::BasisImporter) See @ref building-plugins, @ref cmake-plugins, @ref plugins and @ref file-formats for more information. +@section Trade-BasisImporter-behavior Behavior and limitations + +@subsection Trade-BasisImporter-behavior-types Image types + +You can import all image types supported by `basisu`: (layered) 2D images, +(layered) cube maps, 3D images and videos. They can in turn all have multiple +mip levels. The image type can be determined from @ref texture() and +@ref TextureData::type(). + +For layered 2D images and (layered) cube maps, the array layers and faces are +exposed as an additional image dimension. @ref image3D() will return an +@ref ImageData3D with n z-slices, or 6*n z-slices for cube maps. + +All 3D images will be imported as 2D array textures with as many layers as +depth slices. This unifies the behaviour with Basis compressed KTX2 files that +don't support 3D images in the first place, and avoids confusing behaviour with +mip levels which are always 2-dimensional in Basis compressed images. + +Video files will be imported as multiple 2D images with the same size and level +count. Due to the way video is encoded by Basis Universal, seeking to arbitrary +frames is not allowed. If you call @ref image2D() with non-sequential frame +indices and that frame is not an I-frame, it will print an error and fail. +Restarting from frame 0 is always allowed. + +@subsection Trade-BasisImporter-behavior-multilevel Multilevel images + +Files with multiple mip levels are imported with the largest level first, with +the size of each following level divided by 2, rounded down. Mip chains can be +incomplete, ie. they don't have to extend all the way down to a level of size +1x1. + +Because mip levels in `.basis` files are always 2-dimensional, they wouldn't +halve correctly in the z-dimension for 3D images. If a 3D image with mip levels +is detected, it gets imported as a layered 2D image instead, along with a +warning being printed. + +@subsection Trade-BasisImporter-behavior-cube Cube maps + +Cube map faces are imported in the order +X, -X, +Y, -Y, +Z, -Z as seen from a +left-handed coordinate system (+X is right, +Y is up, +Z is forward). Layered +cube maps are stored as multiple sets of faces, ie. all faces +X through -Z for +the first layer, then all faces of the second layer, etc. + +@m_class{m-block m-warning} + +@par Y-flipping + While all importers for uncompressed image data are performing a Y-flip on + import to have the origin at the bottom (as expected by OpenGL), it's a + non-trivial operation with compressed images. In case of Basis, you can + pass a `-y_flip` flag to the `basisu` tool to Y-flip the image + * *during encoding*, however right now there's no way do so on import. To + inform the user, the importer checks for the Y-flip flag in the file and if + it's not there, prints a warning about the data having wrong orientation. +@par + To account for this on the application side for files that you don't have + control over, flip texture coordinates of the mesh or patch texture data + loading in the shader. + @section Trade-BasisImporter-configuration Plugin-specific configuration Basis allows configuration of the format of loaded compressed data. @@ -143,31 +206,32 @@ you may also use @ref setTargetFormat(). @snippet BasisImporter.cpp target-format-config -There's many options and you should be generally striving for highest-quality -format available on given platform. Detailed description of the choices is -in [Basis Universal README](https://github.com/BinomialLLC/basis_universal#how-to-use-the-system). +There are many options and you should generally be striving for the +highest-quality format available on a given platform. A detailed description of +the choices can be found in the [Basis Universal Wiki](https://github.com/BinomialLLC/basis_universal/wiki/How-to-Deploy-ETC1S-Texture-Content-Using-Basis-Universal). As an example, the following code is a decision making used by @ref magnum-player "magnum-player" based on availability of corresponding OpenGL, OpenGL ES and WebGL extensions, in its full ugly glory: @snippet BasisImporter.cpp gl-extension-checks - +@subsection Trade-BasisImporter-compile-size Reducing compile size -@m_class{m-block m-warning} +To reduce the binary size of the transcoder, Basis Universal supports a set of +preprocessor defines to turn off unneeded features. The Basis Universal Wiki +lists macros to disable specific [target formats](https://github.com/BinomialLLC/basis_universal/wiki/How-to-Use-and-Configure-the-Transcoder#shrinking-the-transcoders-compiled-size) +as well as [KTX2 support](https://github.com/BinomialLLC/basis_universal/wiki/How-to-Use-and-Configure-the-Transcoder#disabling-ktx2-or-zstandard-usage). +If you're building it from source with `BASIS_UNIVERSAL_DIR` set, add the +desired defines before adding `magnum-plugins` as a subfolder: -@par Y-flipping - While all importers for uncompressed image data are doing an Y-flip on - import to have origin at the bottom (as expected by OpenGL), it's a - non-trivial operation with compressed images. In case of Basis, you can - pass a `-y_flip` flag to the `basisu` tool to Y-flip the image - * *during encoding*, however right now there's no way do so on import. To - inform the user, the importer checks for the Y-flip flag in the file and if - it's not there, prints a warning about the data having wrong orientation. -@par - To account for this on the application side for files that you don't have a - control of, flip texture coordinates of the mesh or patch texture data - loading in the shader. +@code{.cmake} +add_definitions( + -DBASISD_SUPPORT_BC7=0 + -DBASISD_SUPPORT_KTX2=0) + +# ... +add_subdirectory(magnum-plugins EXCLUDE_FROM_ALL) +@endcode */ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { public: @@ -183,64 +247,71 @@ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { /* ID kept the same as in Basis itself to make the mapping easy */ /** - * ETC1 RGB. Loaded as @ref CompressedPixelFormat::Etc2RGB8Unorm - * (which ETC1 is a subset of). If the image contains an alpha - * channel, it will be dropped since ETC1 alone doesn't support - * alpha. + * ETC1 RGB. Loaded as @ref CompressedPixelFormat::Etc2RGB8Unorm/ + * @ref CompressedPixelFormat::Etc2RGB8Srgb (which ETC1 is a + * subset of). If the image contains an alpha channel, it will be + * dropped since ETC1 alone doesn't support alpha. */ Etc1RGB = 0, /** - * ETC2 RGBA. Loaded as @ref CompressedPixelFormat::Etc2RGBA8Unorm. + * ETC2 RGBA. Loaded as @ref CompressedPixelFormat::Etc2RGBA8Unorm/ + * @ref CompressedPixelFormat::Etc2RGBA8Srgb. */ Etc2RGBA = 1, /** - * BC1 RGB. Loaded as @ref CompressedPixelFormat::Bc1RGBUnorm. - * Punchthrough alpha mode of @ref CompressedPixelFormat::Bc1RGBAUnorm - * is not supported. + * BC1 RGB. Loaded as @ref CompressedPixelFormat::Bc1RGBUnorm/ + * @ref CompressedPixelFormat::Bc1RGBSrgb. Punchthrough alpha mode + * of BC1 RGBA is not supported. */ Bc1RGB = 2, /** - * BC2 RGBA. Loaded as @ref CompressedPixelFormat::Bc3RGBAUnorm. + * BC3 RGBA. Loaded as @ref CompressedPixelFormat::Bc3RGBAUnorm/ + * @ref CompressedPixelFormat::Bc3RGBASrgb. */ Bc3RGBA = 3, /** BC4 R. Loaded as @ref CompressedPixelFormat::Bc4RUnorm. */ Bc4R = 4, - /** BC5 RG. Loaded as @ref CompressedPixelFormat::Bc5RGUnorm. */ - Bc5RG = 5, - /** - * BC7 RGB (mode 6). Loaded as - * @ref CompressedPixelFormat::Bc7RGBAUnorm, but with alpha values - * set to opaque. + * BC5 RG. Taken from the input red and alpha channel. Loaded as + * @ref CompressedPixelFormat::Bc5RGUnorm. */ - Bc7RGB = 6, + Bc5RG = 5, + + /* Bc7RGB (=6) used to be the mode 6 transcoder which went away + when UASTC was added. The old mode 5 transcoder (=7) is called + BC7_ALT in the transcoder and is only kept around for backward + compatibility but treated exactly the same as BC7_RGBA (=6). */ /** - * BC7 RGBA (mode 5). Loaded as - * @ref CompressedPixelFormat::Bc7RGBAUnorm. + * BC7 RGBA. If no alpha is present, it's set to opaque. Loaded as + * @ref CompressedPixelFormat::Bc7RGBAUnorm/ + * @ref CompressedPixelFormat::Bc7RGBASrgb. */ - Bc7RGBA = 7, + Bc7RGBA = 6, /** * PVRTC1 RGB 4 bpp. Loaded as - * @ref CompressedPixelFormat::PvrtcRGB4bppUnorm. + * @ref CompressedPixelFormat::PvrtcRGB4bppUnorm/ + * @ref CompressedPixelFormat::PvrtcRGB4bppSrgb. */ PvrtcRGB4bpp = 8, /** * PVRTC1 RGBA 4 bpp. Loaded as - * @ref CompressedPixelFormat::PvrtcRGBA4bppUnorm. + * @ref CompressedPixelFormat::PvrtcRGBA4bppUnorm/ + * @ref CompressedPixelFormat::PvrtcRGBA4bppSrgb. */ PvrtcRGBA4bpp = 9, /** * ASTC 4x4 RGBA. Loaded as - * @ref CompressedPixelFormat::Astc4x4RGBAUnorm. + * @ref CompressedPixelFormat::Astc4x4RGBAUnorm/ + * @ref CompressedPixelFormat::Astc4x4RGBASrgb. */ Astc4x4RGBA = 10, @@ -248,8 +319,9 @@ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { /** * Uncompressed 32-bit RGBA. Loaded as - * @ref PixelFormat::RGBA8Unorm. If no concrete format is - * specified, the importer will fall back to this. + * @ref PixelFormat::RGBA8Unorm/@ref PixelFormat::RGBA8Srgb. If no + * concrete format is specified, the importer will fall back to + * this. */ RGBA8 = 13, @@ -260,13 +332,12 @@ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { have enums for those */ /** - * EAC unsigned red component. Loaded as - * @ref CompressedPixelFormat::EacR11Unorm. + * EAC R. Loaded as @ref CompressedPixelFormat::EacR11Unorm. */ EacR = 20, /** - * EAC unsigned red and green component. Loaded as + * EAC RG. Taken from the input red and alpha channel. Loaded as * @ref CompressedPixelFormat::EacRG11Unorm. */ EacRG = 21, @@ -300,8 +371,6 @@ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { void setTargetFormat(TargetFormat format); private: - struct State; - MAGNUM_BASISIMPORTER_LOCAL ImporterFeatures doFeatures() const override; MAGNUM_BASISIMPORTER_LOCAL bool doIsOpened() const override; MAGNUM_BASISIMPORTER_LOCAL void doClose() override; @@ -311,7 +380,18 @@ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { MAGNUM_BASISIMPORTER_LOCAL UnsignedInt doImage2DLevelCount(UnsignedInt id) override; MAGNUM_BASISIMPORTER_LOCAL Containers::Optional doImage2D(UnsignedInt id, UnsignedInt level) override; + MAGNUM_BASISIMPORTER_LOCAL UnsignedInt doImage3DCount() const override; + MAGNUM_BASISIMPORTER_LOCAL UnsignedInt doImage3DLevelCount(UnsignedInt id) override; + MAGNUM_BASISIMPORTER_LOCAL Containers::Optional doImage3D(UnsignedInt id, UnsignedInt level) override; + + MAGNUM_BASISIMPORTER_LOCAL UnsignedInt doTextureCount() const override; + MAGNUM_BASISIMPORTER_LOCAL Containers::Optional doTexture(UnsignedInt id) override; + + struct State; Containers::Pointer _state; + + template + Containers::Optional> doImage(UnsignedInt id, UnsignedInt level); }; }} diff --git a/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp b/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp index 4e343a8ea..70cbff499 100644 --- a/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp +++ b/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp @@ -32,12 +32,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include "configure.h" @@ -47,20 +49,42 @@ struct BasisImporterTest: TestSuite::Tester { explicit BasisImporterTest(); void empty(); - void invalid(); + + void invalidHeader(); + void invalidFile(); + void fileTooShort(); + void unconfigured(); void invalidConfiguredFormat(); - void fileTooShort(); + void unsupportedFormat(); void transcodingFailure(); + void nonBasisKtx(); + + void texture(); void rgbUncompressed(); void rgbUncompressedNoFlip(); + void rgbUncompressedLinear(); void rgbaUncompressed(); + void rgbaUncompressedUastc(); void rgbaUncompressedMultipleImages(); void rgb(); void rgba(); + void linear(); + + void array2D(); + void array2DMipmaps(); + void video(); + void image3D(); + void image3DMipmaps(); + void cubeMap(); + void cubeMapArray(); + + void videoSeeking(); + void videoVerbose(); + void openMemory(); void openSameTwice(); void openDifferent(); @@ -71,36 +95,115 @@ struct BasisImporterTest: TestSuite::Tester { }; constexpr struct { + const char* name; + const char* extension; +} FileTypeData[] { + {"Basis", ".basis"}, + {"KTX2", ".ktx2"} +}; + +constexpr struct { + const char* name; + const Containers::ArrayView data; + const char* message; +} InvalidHeaderData[] { + /* GCC 4.8 needs the explicit cast :( */ + {"Invalid", Containers::arrayView("NotAValidFile"), "invalid basis header"}, + {"Invalid basis header", Containers::arrayView("sB\xff\xff"), "invalid basis header"}, + {"Invalid KTX2 identifier", Containers::arrayView("\xabKTX 30\xbb\r\n\x1a\n"), "invalid basis header"}, + {"Invalid KTX2 header", Containers::arrayView("\xabKTX 20\xbb\r\n\x1a\n\xff\xff\xff\xff"), "invalid KTX2 header, or not Basis compressed"} +}; + +constexpr struct { + const char* name; const char* file; - const char* fileAlpha; + std::size_t offset; + const char value; + const char* message; +} InvalidFileData[] { + /* Change ktx2_etc1s_global_data_header::m_endpoint_count */ + {"Corrupt KTX2 supercompression data", "rgb.ktx2", 184, 0x00, "bad KTX2 file"}, + /* Changing anything in basis_file_header after m_header_crc16 makes the + CRC16 check fail. Only the header checksum is currently checked, not the + data checksum, so patching slice metadata still works. */ + {"Invalid header CRC16", "rgb.basis", 23, 0x7f, "bad basis file"}, + /* Change basis_slice_desc::m_orig_width of slice 0 */ + {"Mismatching array sizes", "rgba-array.basis", 82, 0x7f, "image slices have mismatching size, expected 127 by 27 but got 63 by 27"}, + /* Change basis_slice_desc::m_orig_width of slice 0 */ + {"Mismatching video sizes", "rgba-video.basis", 82, 0x7f, "image slices have mismatching size, expected 127 by 27 but got 63 by 27"}, + /* We can't patch m_tex_type in the header because of the CRC check, so we + need dedicated files for the cube map checks */ + {"Invalid face count", "invalid-cube-face-count.basis", ~std::size_t(0), 0, "cube map face count must be a multiple of 6 but got 3"}, + {"Face not square", "invalid-cube-face-size.basis", ~std::size_t(0), 0, "cube map must be square but got 15 by 6"} +}; + +constexpr struct { + const char* name; + const char* file; + const std::size_t size; + const char* message; +} FileTooShortData[] { + {"Basis", "rgb.basis", 64, "invalid basis header"}, + {"KTX2", "rgb.ktx2", 64, "invalid KTX2 header, or not Basis compressed"} +}; + +const struct { + const char* name; + const char* fileBase; + const TextureType type; +} TextureData[]{ + {"2D", "rgb", TextureType::Texture2D}, + {"2D array", "rgba-array", TextureType::Texture2DArray}, + {"Cube map", "rgba-cubemap", TextureType::CubeMap}, + {"Cube map array", "rgba-cubemap-array", TextureType::CubeMapArray}, + {"3D", "rgba-3d", TextureType::Texture3D}, + {"3D mipmaps", "rgba-3d-mips", TextureType::Texture3D}, + {"Video", "rgba-video", TextureType::Texture2D} +}; + +constexpr struct { + const char* fileBase; + const char* fileBaseAlpha; + const char* fileBaseLinear; + const Vector2i expectedSize; const char* suffix; const CompressedPixelFormat expectedFormat; - const Vector2i expectedSize; + const CompressedPixelFormat expectedLinearFormat; } FormatData[] { - {"rgb.basis", "rgba.basis", - "Etc1RGB", CompressedPixelFormat::Etc2RGB8Unorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "Etc2RGBA", CompressedPixelFormat::Etc2RGBA8Unorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "Bc1RGB", CompressedPixelFormat::Bc1RGBUnorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "Bc3RGBA", CompressedPixelFormat::Bc3RGBAUnorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "Bc4R", CompressedPixelFormat::Bc4RUnorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "Bc5RG", CompressedPixelFormat::Bc5RGUnorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "Bc7RGB", CompressedPixelFormat::Bc7RGBAUnorm, {63, 27}}, - {"rgb-pow2.basis", "rgba-pow2.basis", - "PvrtcRGB4bpp", CompressedPixelFormat::PvrtcRGB4bppUnorm, {64, 32}}, - {"rgb-pow2.basis", "rgba-pow2.basis", - "PvrtcRGBA4bpp", CompressedPixelFormat::PvrtcRGBA4bppUnorm, {64, 32}}, - {"rgb.basis", "rgba.basis", - "Astc4x4RGBA", CompressedPixelFormat::Astc4x4RGBAUnorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "EacR", CompressedPixelFormat::EacR11Unorm, {63, 27}}, - {"rgb.basis", "rgba.basis", - "EacRG", CompressedPixelFormat::EacRG11Unorm, {63, 27}} + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Etc1RGB", CompressedPixelFormat::Etc2RGB8Srgb, CompressedPixelFormat::Etc2RGB8Unorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Etc2RGBA", CompressedPixelFormat::Etc2RGBA8Srgb, CompressedPixelFormat::Etc2RGBA8Unorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Bc1RGB", CompressedPixelFormat::Bc1RGBSrgb, CompressedPixelFormat::Bc1RGBUnorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Bc3RGBA", CompressedPixelFormat::Bc3RGBASrgb, CompressedPixelFormat::Bc3RGBAUnorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Bc4R", CompressedPixelFormat::Bc4RUnorm, CompressedPixelFormat::Bc4RUnorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Bc5RG", CompressedPixelFormat::Bc5RGUnorm, CompressedPixelFormat::Bc5RGUnorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Bc7RGBA", CompressedPixelFormat::Bc7RGBASrgb, CompressedPixelFormat::Bc7RGBAUnorm}, + {"rgb-pow2", "rgba-pow2", "rgb-linear-pow2", {64, 32}, + "PvrtcRGB4bpp", CompressedPixelFormat::PvrtcRGB4bppSrgb, CompressedPixelFormat::PvrtcRGB4bppUnorm}, + {"rgb-pow2", "rgba-pow2", "rgb-linear-pow2", {64, 32}, + "PvrtcRGBA4bpp", CompressedPixelFormat::PvrtcRGBA4bppSrgb, CompressedPixelFormat::PvrtcRGBA4bppUnorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "Astc4x4RGBA", CompressedPixelFormat::Astc4x4RGBASrgb, CompressedPixelFormat::Astc4x4RGBAUnorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "EacR", CompressedPixelFormat::EacR11Unorm, CompressedPixelFormat::EacR11Unorm}, + {"rgb", "rgba", "rgb-linear", {63, 27}, + "EacRG", CompressedPixelFormat::EacRG11Unorm, CompressedPixelFormat::EacRG11Unorm} +}; + +const struct { + const char* name; + const char* file; +} VideoSeekingData[]{ + {"Basis ETC1S", "rgba-video.basis"}, + {"KTX2 ETC1S", "rgba-video.ktx2"}, + {"Basis UASTC", "rgba-video-uastc.basis"}, + {"KTX2 UASTC", "rgba-video-uastc.ktx2"} }; /* Shared among all plugins that implement data copying optimizations */ @@ -120,21 +223,53 @@ const struct { }; BasisImporterTest::BasisImporterTest() { - addTests({&BasisImporterTest::empty, - &BasisImporterTest::invalid, - &BasisImporterTest::unconfigured, + addTests({&BasisImporterTest::empty}); + + addInstancedTests({&BasisImporterTest::invalidHeader}, + Containers::arraySize(InvalidHeaderData)); + + addInstancedTests({&BasisImporterTest::invalidFile}, + Containers::arraySize(InvalidFileData)); + + addInstancedTests({&BasisImporterTest::fileTooShort}, + Containers::arraySize(FileTooShortData)); + + addTests({&BasisImporterTest::unconfigured, &BasisImporterTest::invalidConfiguredFormat, - &BasisImporterTest::fileTooShort, + &BasisImporterTest::unsupportedFormat, &BasisImporterTest::transcodingFailure, + &BasisImporterTest::nonBasisKtx}); - &BasisImporterTest::rgbUncompressed, - &BasisImporterTest::rgbUncompressedNoFlip, - &BasisImporterTest::rgbaUncompressed, - &BasisImporterTest::rgbaUncompressedMultipleImages}); + addInstancedTests({&BasisImporterTest::texture}, + Containers::arraySize(TextureData)); + + addInstancedTests({&BasisImporterTest::rgbUncompressed, + &BasisImporterTest::rgbUncompressedNoFlip, + &BasisImporterTest::rgbUncompressedLinear, + &BasisImporterTest::rgbaUncompressed, + &BasisImporterTest::rgbaUncompressedUastc}, + Containers::arraySize(FileTypeData)); + + addTests({&BasisImporterTest::rgbaUncompressedMultipleImages}); addInstancedTests({&BasisImporterTest::rgb, - &BasisImporterTest::rgba}, - Containers::arraySize(FormatData)); + &BasisImporterTest::rgba, + &BasisImporterTest::linear}, + Containers::arraySize(FormatData)); + + addInstancedTests({&BasisImporterTest::array2D, + &BasisImporterTest::array2DMipmaps, + &BasisImporterTest::video, + &BasisImporterTest::image3D, + &BasisImporterTest::image3DMipmaps, + &BasisImporterTest::cubeMap, + &BasisImporterTest::cubeMapArray}, + Containers::arraySize(FileTypeData)); + +addInstancedTests({&BasisImporterTest::videoSeeking}, + Containers::arraySize(VideoSeekingData)); + + addTests({&BasisImporterTest::videoVerbose}); addInstancedTests({&BasisImporterTest::openMemory}, Containers::arraySize(OpenMemoryData)); @@ -169,13 +304,56 @@ void BasisImporterTest::empty() { CORRADE_COMPARE(out.str(), "Trade::BasisImporter::openData(): the file is empty\n"); } -void BasisImporterTest::invalid() { +void BasisImporterTest::invalidHeader() { + auto&& data = InvalidHeaderData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporter"); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openData(data.data)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::BasisImporter::openData(): {}\n", data.message)); +} + +void BasisImporterTest::invalidFile() { + auto&& data = InvalidFileData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporter"); + + auto basisData = Utility::Directory::read( + Utility::Directory::join(BASISIMPORTER_TEST_DIR, data.file)); + CORRADE_VERIFY(basisData); + + if(data.offset != ~std::size_t(0)) { + CORRADE_VERIFY(data.offset < basisData.size()); + basisData[data.offset] = data.value; + } + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openData(basisData)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::BasisImporter::openData(): {}\n", data.message)); +} + +void BasisImporterTest::fileTooShort() { + auto&& data = FileTooShortData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + Containers::Pointer importer = _manager.instantiate("BasisImporter"); + auto basisData = Utility::Directory::read( + Utility::Directory::join(BASISIMPORTER_TEST_DIR, data.file)); + std::ostringstream out; Error redirectError{&out}; - CORRADE_VERIFY(!importer->openData("NotABasisFile")); - CORRADE_COMPARE(out.str(), "Trade::BasisImporter::openData(): invalid header\n"); + /* Shorten the data */ + CORRADE_INTERNAL_ASSERT(data.size < basisData.size()); + CORRADE_VERIFY(!importer->openData(basisData.prefix(data.size))); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::BasisImporter::openData(): {}\n", data.message)); } void BasisImporterTest::unconfigured() { @@ -194,7 +372,7 @@ void BasisImporterTest::unconfigured() { } CORRADE_VERIFY(image); CORRADE_VERIFY(!image->isCompressed()); - CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning either load the plugin via one of its BasisImporterEtc1RGB, ... aliases, or explicitly set the format option in plugin configuration.\n"); @@ -207,7 +385,7 @@ void BasisImporterTest::unconfigured() { CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels()), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 55.67f, 6.589f})); + (DebugTools::CompareImageToFile{_manager, 58.334f, 6.622f})); } void BasisImporterTest::invalidConfiguredFormat() { @@ -220,26 +398,26 @@ void BasisImporterTest::invalidConfiguredFormat() { importer->configuration().setValue("format", "Banana"); CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): invalid transcoding target format Banana, expected to be one of EacR, EacRG, Etc1RGB, Etc2RGBA, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, RGBA8\n"); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): invalid transcoding target format Banana, expected to be one of EacR, EacRG, Etc1RGB, Etc2RGBA, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, RGBA8\n"); } -void BasisImporterTest::fileTooShort() { +void BasisImporterTest::unsupportedFormat() { + #if !defined(BASISD_SUPPORT_BC7) || BASISD_SUPPORT_BC7 + /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's + NOT the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ + CORRADE_SKIP("BC7 is compiled into Basis, can't test unsupported target format error."); + #endif + Containers::Pointer importer = _manager.instantiate("BasisImporter"); - auto basisData = Utility::Directory::read( - Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb.basis")); + CORRADE_VERIFY(importer->openFile( + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb.basis"))); std::ostringstream out; Error redirectError{&out}; + importer->configuration().setValue("format", "Bc7RGBA"); + CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_VERIFY(!importer->openData(basisData.prefix(64))); - - /* Corrupt the header */ - basisData[100] = 100; - CORRADE_VERIFY(!importer->openData(basisData)); - - CORRADE_COMPARE(out.str(), - "Trade::BasisImporter::openData(): invalid header\n" - "Trade::BasisImporter::openData(): bad basis file\n"); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): Basis Universal was compiled without support for Bc7RGBA\n"); } void BasisImporterTest::transcodingFailure() { @@ -249,20 +427,97 @@ void BasisImporterTest::transcodingFailure() { std::ostringstream out; Error redirectError{&out}; - /* PVRTC1 requires power of 2 image dimensions, but rgb.basis is 27x63, + /* PVRTC1 requires power of 2 image dimensions, but rgb.basis is 63x27, hence basis will fail during transcoding */ Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(!image); CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): transcoding failed\n"); } +void BasisImporterTest::nonBasisKtx() { + Containers::Pointer importer = _manager.instantiate("BasisImporter"); + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgba.ktx2"))); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::openData(): invalid KTX2 header, or not Basis compressed\n"); +} + +void BasisImporterTest::texture() { + auto&& data = TextureData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporter"); + + for(const auto& fileType: FileTypeData) { + CORRADE_ITERATION(fileType.name); + + const bool isKtx2 = std::string{fileType.name} == "KTX2"; + const bool is3D = data.type == TextureType::Texture3D; + /* basisu saves volume textures as KTX2 2D arrays, and we import 3D + basis files as 2D arrays, too */ + const TextureType realType = is3D ? TextureType::Texture2DArray : data.type; + + std::ostringstream out; + Warning redirectWarning{&out}; + + CORRADE_VERIFY(importer->openFile( + Utility::Directory::join(BASISIMPORTER_TEST_DIR, std::string{data.fileBase} + fileType.extension))); + if(!isKtx2 && is3D) + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::openData(): importing 3D texture as a 2D array texture\n"); + else + CORRADE_COMPARE(out.str(), ""); + + const Vector3ui counts{ + importer->image1DCount(), + importer->image2DCount(), + importer->image3DCount() + }; + const UnsignedInt total = counts.sum(); + + CORRADE_VERIFY(total > 0); + CORRADE_COMPARE(counts.max(), total); + CORRADE_COMPARE(importer->textureCount(), total); + + for(UnsignedInt i = 0; i != total; ++i) { + CORRADE_ITERATION(i); + + const auto texture = importer->texture(i); + CORRADE_VERIFY(texture); + CORRADE_COMPARE(texture->minificationFilter(), SamplerFilter::Linear); + CORRADE_COMPARE(texture->magnificationFilter(), SamplerFilter::Linear); + CORRADE_COMPARE(texture->mipmapFilter(), SamplerMipmap::Linear); + CORRADE_COMPARE(texture->wrapping(), Math::Vector3{SamplerWrapping::Repeat}); + CORRADE_COMPARE(texture->image(), i); + CORRADE_COMPARE(texture->importerState(), nullptr); + CORRADE_COMPARE(texture->type(), realType); + } + + UnsignedInt dimensions; + switch(realType) { + case TextureType::Texture2D: + dimensions = 2; + break; + case TextureType::Texture2DArray: + case TextureType::CubeMap: + case TextureType::CubeMapArray: + dimensions = 3; + break; + /* No 1D/3D (array) allowed */ + default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + } + CORRADE_COMPARE(counts[dimensions - 1], total); + } +} + void BasisImporterTest::rgbUncompressed() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); - CORRADE_VERIFY(importer); CORRADE_COMPARE(importer->configuration().value("format"), "RGBA8"); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, - "rgb.basis"))); + std::string{"rgb"} + data.extension))); CORRADE_COMPARE(importer->image2DCount(), 1); Containers::Optional image; @@ -275,7 +530,7 @@ void BasisImporterTest::rgbUncompressed() { /* There should be no Y-flip warning as the image is pre-flipped */ CORRADE_COMPARE(out.str(), ""); CORRADE_VERIFY(!image->isCompressed()); - CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) @@ -286,16 +541,16 @@ void BasisImporterTest::rgbUncompressed() { CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels()), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 55.67f, 6.589f})); + (DebugTools::CompareImageToFile{_manager, 58.334f, 6.622f})); } void BasisImporterTest::rgbUncompressedNoFlip() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); - CORRADE_VERIFY(importer); - CORRADE_COMPARE(importer->configuration().value("format"), - "RGBA8"); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, - "rgb-noflip.basis"))); + std::string{"rgb-noflip"} + data.extension))); CORRADE_COMPARE(importer->image2DCount(), 1); Containers::Optional image; @@ -307,7 +562,7 @@ void BasisImporterTest::rgbUncompressedNoFlip() { CORRADE_VERIFY(image); CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): the image was not encoded Y-flipped, imported data will have wrong orientation\n"); CORRADE_VERIFY(!image->isCompressed()); - CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) @@ -318,16 +573,16 @@ void BasisImporterTest::rgbUncompressedNoFlip() { CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels().flipped<0>()), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 49.67f, 8.326f})); + (DebugTools::CompareImageToFile{_manager, 51.334f, 8.643f})); } -void BasisImporterTest::rgbaUncompressed() { +void BasisImporterTest::rgbUncompressedLinear() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); - CORRADE_VERIFY(importer); - CORRADE_COMPARE(importer->configuration().value("format"), - "RGBA8"); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, - "rgba.basis"))); + std::string{"rgb-linear"} + data.extension))); CORRADE_COMPARE(importer->image2DCount(), 1); Containers::Optional image = importer->image2D(0); @@ -336,6 +591,32 @@ void BasisImporterTest::rgbaUncompressed() { CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels()), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 61.0f, 6.321f})); +} + +void BasisImporterTest::rgbaUncompressed() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba"} + data.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) @@ -344,19 +625,42 @@ void BasisImporterTest::rgbaUncompressed() { CORRADE_COMPARE_WITH(image->pixels(), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 78.3f, 8.31f})); + (DebugTools::CompareImageToFile{_manager, 94.0f, 8.039f})); +} + +void BasisImporterTest::rgbaUncompressedUastc() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-uastc"} + data.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + CORRADE_COMPARE_WITH(image->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + /* There are insignificant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 4.75f, 0.576f})); } void BasisImporterTest::rgbaUncompressedMultipleImages() { Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); - CORRADE_VERIFY(importer); - CORRADE_COMPARE(importer->configuration().value("format"), - "RGBA8"); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-2images-mips.basis"))); CORRADE_COMPARE(importer->image2DCount(), 2); CORRADE_COMPARE(importer->image2DLevelCount(0), 3); - CORRADE_COMPARE(importer->image2DLevelCount(1), 3); + CORRADE_COMPARE(importer->image2DLevelCount(1), 2); Containers::Optional image0 = importer->image2D(0); Containers::Optional image0l1 = importer->image2D(0, 1); @@ -367,24 +671,21 @@ void BasisImporterTest::rgbaUncompressedMultipleImages() { Containers::Optional image1 = importer->image2D(1); Containers::Optional image1l1 = importer->image2D(1, 1); - Containers::Optional image1l2 = importer->image2D(1, 2); CORRADE_VERIFY(image1); CORRADE_VERIFY(image1l1); - CORRADE_VERIFY(image1l2); for(auto* image: {&*image0, &*image0l1, &*image0l2, - &*image1, &*image1l1, &*image1l2}) { + &*image1, &*image1l1}) { CORRADE_ITERATION(image->size()); CORRADE_VERIFY(!image->isCompressed()); - CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); } CORRADE_COMPARE(image0->size(), (Vector2i{63, 27})); CORRADE_COMPARE(image0l1->size(), (Vector2i{31, 13})); CORRADE_COMPARE(image0l2->size(), (Vector2i{15, 6})); - CORRADE_COMPARE(image1->size(), (Vector2i{27, 63})); - CORRADE_COMPARE(image1l1->size(), (Vector2i{13, 31})); - CORRADE_COMPARE(image1l2->size(), (Vector2i{6, 15})); + CORRADE_COMPARE(image1->size(), (Vector2i{13, 31})); + CORRADE_COMPARE(image1l1->size(), (Vector2i{6, 15})); if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); @@ -395,25 +696,19 @@ void BasisImporterTest::rgbaUncompressedMultipleImages() { one image alone as there's more to compress */ CORRADE_COMPARE_WITH(image0->pixels(), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), - (DebugTools::CompareImageToFile{_manager, 88.25f, 8.357f})); + (DebugTools::CompareImageToFile{_manager, 96.0f, 8.11f})); CORRADE_COMPARE_WITH(image0l1->pixels(), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), - (DebugTools::CompareImageToFile{_manager, 75.25f, 14.85f})); + (DebugTools::CompareImageToFile{_manager, 75.75f, 14.077f})); CORRADE_COMPARE_WITH(image0l2->pixels(), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"), - (DebugTools::CompareImageToFile{_manager, 64.5f, 23.85f})); + (DebugTools::CompareImageToFile{_manager, 65.0f, 23.487f})); CORRADE_COMPARE_WITH(image1->pixels(), - Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x63.png"), - (DebugTools::CompareImageToFile{_manager, 87.8f, 9.984f})); - /* Rotating the pixels so we don't need to store the ground truth twice. - Somehow it compresses differently for those, tho (I would expect the - compression to be invariant of the orientation). */ - CORRADE_COMPARE_WITH((image1l1->pixels().transposed<0, 1>()), - Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), - (DebugTools::CompareImageToFile{_manager, 82.5f, 33.27f})); - CORRADE_COMPARE_WITH((image1l2->pixels().transposed<0, 1>()), - Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"), - (DebugTools::CompareImageToFile{_manager, 85.25f, 40.15f})); + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-13x31.png"), + (DebugTools::CompareImageToFile{_manager, 55.5f, 12.305f})); + CORRADE_COMPARE_WITH(image1l1->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-6x15.png"), + (DebugTools::CompareImageToFile{_manager, 68.25f, 21.792f})); } void BasisImporterTest::rgb() { @@ -421,8 +716,8 @@ void BasisImporterTest::rgb() { #if defined(BASISD_SUPPORT_BC7) && !BASISD_SUPPORT_BC7 /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's - the case. This assumes -DBASISD_SUPPORT_*=0 issupplied globally. */ - if(formatData.expectedFormat == CompressedPixelFormat::Bc7RGBAUnorm) + the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ + if(formatData.expectedFormat == CompressedPixelFormat::Bc7RGBASrgb) CORRADE_SKIP("This format is not compiled into Basis."); #endif @@ -430,21 +725,25 @@ void BasisImporterTest::rgb() { setTestCaseDescription(formatData.suffix); Containers::Pointer importer = _manager.instantiate(pluginName); - CORRADE_VERIFY(importer); CORRADE_COMPARE(importer->configuration().value("format"), formatData.suffix); - CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, - formatData.file))); - CORRADE_COMPARE(importer->image2DCount(), 1); - Containers::Optional image = importer->image2D(0); - CORRADE_VERIFY(image); - CORRADE_VERIFY(image->isCompressed()); - CORRADE_COMPARE(image->compressedFormat(), formatData.expectedFormat); - CORRADE_COMPARE(image->size(), formatData.expectedSize); - /** @todo remove this once CompressedImage etc. tests for data size on its - own / we're able to decode the data ourselves */ - CORRADE_COMPARE(image->data().size(), compressedBlockDataSize(formatData.expectedFormat)*((image->size() + compressedBlockSize(formatData.expectedFormat).xy() - Vector2i{1})/compressedBlockSize(formatData.expectedFormat).xy()).product()); + for(const auto& fileType: FileTypeData) { + CORRADE_ITERATION(fileType.name); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{formatData.fileBase} + fileType.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), formatData.expectedFormat); + CORRADE_COMPARE(image->size(), formatData.expectedSize); + /** @todo remove this once CompressedImage etc. tests for data size on its + own / we're able to decode the data ourselves */ + CORRADE_COMPARE(image->data().size(), compressedBlockDataSize(formatData.expectedFormat)*((image->size() + compressedBlockSize(formatData.expectedFormat).xy() - Vector2i{1})/compressedBlockSize(formatData.expectedFormat).xy()).product()); + } } void BasisImporterTest::rgba() { @@ -452,8 +751,45 @@ void BasisImporterTest::rgba() { #if defined(BASISD_SUPPORT_BC7) && !BASISD_SUPPORT_BC7 /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's - the case. This assumes -DBASISD_SUPPORT_*=0 issupplied globally. */ - if(formatData.expectedFormat == CompressedPixelFormat::Bc7RGBAUnorm) + the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ + if(formatData.expectedFormat == CompressedPixelFormat::Bc7RGBASrgb) + CORRADE_SKIP("This format is not compiled into Basis."); + #endif + + const std::string pluginName = "BasisImporter" + std::string(formatData.suffix); + setTestCaseDescription(formatData.suffix); + + Containers::Pointer importer = _manager.instantiate(pluginName); + CORRADE_COMPARE(importer->configuration().value("format"), + formatData.suffix); + + for(const auto& fileType: FileTypeData) { + CORRADE_ITERATION(fileType.name); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{formatData.fileBaseAlpha} + fileType.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), formatData.expectedFormat); + CORRADE_COMPARE(image->size(), formatData.expectedSize); + /** @todo remove this once CompressedImage etc. tests for data size on its + own / we're able to decode the data ourselves */ + CORRADE_COMPARE(image->data().size(), compressedBlockDataSize(formatData.expectedFormat)*((image->size() + compressedBlockSize(formatData.expectedFormat).xy() - Vector2i{1})/compressedBlockSize(formatData.expectedFormat).xy()).product()); + } +} + +void BasisImporterTest::linear() { + auto& formatData = FormatData[testCaseInstanceId()]; + + /* Test linear formats, sRGB was tested in rgb() */ + + #if defined(BASISD_SUPPORT_BC7) && !BASISD_SUPPORT_BC7 + /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's + the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ + if(formatData.expectedLinearFormat == CompressedPixelFormat::Bc7RGBAUnorm) CORRADE_SKIP("This format is not compiled into Basis."); #endif @@ -461,21 +797,379 @@ void BasisImporterTest::rgba() { setTestCaseDescription(formatData.suffix); Containers::Pointer importer = _manager.instantiate(pluginName); - CORRADE_VERIFY(importer); CORRADE_COMPARE(importer->configuration().value("format"), formatData.suffix); + + for(const auto& fileType: FileTypeData) { + CORRADE_ITERATION(fileType.name); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{formatData.fileBaseLinear} + fileType.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), formatData.expectedLinearFormat); + CORRADE_COMPARE(image->size(), formatData.expectedSize); + /** @todo remove this once CompressedImage etc. tests for data size on its + own / we're able to decode the data ourselves */ + CORRADE_COMPARE(image->data().size(), compressedBlockDataSize(formatData.expectedLinearFormat)*((image->size() + compressedBlockSize(formatData.expectedLinearFormat).xy() - Vector2i{1})/compressedBlockSize(formatData.expectedLinearFormat).xy()).product()); + } +} + +void BasisImporterTest::array2D() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, - formatData.fileAlpha))); - CORRADE_COMPARE(importer->image2DCount(), 1); + std::string{"rgba-array"} + data.extension))); - Containers::Optional image = importer->image2D(0); + CORRADE_COMPARE(importer->image3DCount(), 1); + Containers::Optional image = importer->image3D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + + CORRADE_COMPARE(image->size(), (Vector3i{63, 27, 3})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(image->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 8.064f})); + CORRADE_COMPARE_WITH(image->pixels()[1], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.481f})); + CORRADE_COMPARE_WITH(image->pixels()[2], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 8.426f})); +} + +void BasisImporterTest::array2DMipmaps() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-array-mips"} + data.extension))); + + Containers::Optional levels[3]; + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), Containers::arraySize(levels)); + + for(std::size_t i = 0; i != Containers::arraySize(levels); ++i) { + CORRADE_ITERATION(i); + levels[i] = importer->image3D(0, i); + CORRADE_VERIFY(levels[i]); + + CORRADE_VERIFY(!levels[i]->isCompressed()); + CORRADE_COMPARE(levels[i]->format(), PixelFormat::RGBA8Srgb); + CORRADE_COMPARE(levels[i]->size(), (Vector3i{Vector2i{63, 27} >> i, 3})); + } + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(levels[0]->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + (DebugTools::CompareImageToFile{_manager, 96.0f, 8.027f})); + CORRADE_COMPARE_WITH(levels[0]->pixels()[1], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.482f})); + CORRADE_COMPARE_WITH(levels[0]->pixels()[2], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 90.0f, 8.399f})); + + /* Only testing the first layer's mips so we don't need all the slices' + mips as ground truth data, too */ + CORRADE_COMPARE_WITH(levels[1]->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), + (DebugTools::CompareImageToFile{_manager, 75.75f, 14.132f})); + CORRADE_COMPARE_WITH(levels[2]->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"), + (DebugTools::CompareImageToFile{_manager, 65.0f, 23.47f})); +} + +void BasisImporterTest::video() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-video"} + data.extension))); + + Containers::Optional frames[3]; + + CORRADE_COMPARE(importer->image2DCount(), Containers::arraySize(frames)); + + for(UnsignedInt i = 0; i != Containers::arraySize(frames); ++i) { + frames[i] = importer->image2D(i); + CORRADE_VERIFY(frames[i]); + CORRADE_VERIFY(!frames[i]->isCompressed()); + CORRADE_COMPARE(frames[i]->format(), PixelFormat::RGBA8Srgb); + CORRADE_COMPARE(frames[i]->size(), (Vector2i{63, 27})); + } + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(frames[0]->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + (DebugTools::CompareImageToFile{_manager, 96.25f, 8.198f})); + CORRADE_COMPARE_WITH(frames[1]->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.507f})); + CORRADE_COMPARE_WITH(frames[2]->pixels(), + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 76.0f, 8.311f})); +} + +void BasisImporterTest::image3D() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-3d"} + data.extension))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + Containers::Optional image = importer->image3D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + + CORRADE_COMPARE(image->size(), (Vector3i{63, 27, 3})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(image->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 8.064f})); + CORRADE_COMPARE_WITH(image->pixels()[1], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.481f})); + CORRADE_COMPARE_WITH(image->pixels()[2], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 8.426f})); +} + +void BasisImporterTest::image3DMipmaps() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Data is identical to array2DMipmaps. Mip levels in basis are per 2D + image, for 3D images they consequently don't halve in the z-dimension. + The importer prints a warning (unless it's a KTX2 file, those don't + indicate 3D images at all) and imports as Texture2DArray. The texture + type is tested in texture(). */ + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-3d-mips"} + data.extension))); + + Containers::Optional levels[3]; + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), Containers::arraySize(levels)); + + for(std::size_t i = 0; i != Containers::arraySize(levels); ++i) { + CORRADE_ITERATION(i); + levels[i] = importer->image3D(0, i); + CORRADE_VERIFY(levels[i]); + + CORRADE_VERIFY(!levels[i]->isCompressed()); + CORRADE_COMPARE(levels[i]->format(), PixelFormat::RGBA8Srgb); + CORRADE_COMPARE(levels[i]->size(), (Vector3i{Vector2i{63, 27} >> i, 3})); + } + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(levels[0]->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), + (DebugTools::CompareImageToFile{_manager, 96.0f, 8.027f})); + CORRADE_COMPARE_WITH(levels[0]->pixels()[1], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.482f})); + CORRADE_COMPARE_WITH(levels[0]->pixels()[2], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 90.0f, 8.399f})); + + /* Only testing the first layer's mips so we don't need all the slices' + mips as ground truth data, too */ + CORRADE_COMPARE_WITH(levels[1]->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), + (DebugTools::CompareImageToFile{_manager, 75.75f, 14.132f})); + CORRADE_COMPARE_WITH(levels[2]->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"), + (DebugTools::CompareImageToFile{_manager, 65.0f, 23.47f})); +} + +void BasisImporterTest::cubeMap() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-cubemap"} + data.extension))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + Containers::Optional image = importer->image3D(0); CORRADE_VERIFY(image); - CORRADE_VERIFY(image->isCompressed()); - CORRADE_COMPARE(image->compressedFormat(), formatData.expectedFormat); - CORRADE_COMPARE(image->size(), formatData.expectedSize); - /** @todo remove this once CompressedImage etc. tests for data size on its - own / we're able to decode the data ourselves */ - CORRADE_COMPARE(image->data().size(), compressedBlockDataSize(formatData.expectedFormat)*((image->size() + compressedBlockSize(formatData.expectedFormat).xy() - Vector2i{1})/compressedBlockSize(formatData.expectedFormat).xy()).product()); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + + CORRADE_COMPARE(image->size(), (Vector3i{27, 27, 6})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(image->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 10.83f})); + CORRADE_COMPARE_WITH(image->pixels()[1], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.972f})); + CORRADE_COMPARE_WITH(image->pixels()[2], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 10.591f})); + CORRADE_COMPARE_WITH(image->pixels()[3], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 10.83f})); + CORRADE_COMPARE_WITH(image->pixels()[4], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.972f})); + CORRADE_COMPARE_WITH(image->pixels()[5], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 10.591f})); +} + +void BasisImporterTest::cubeMapArray() { + auto& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + std::string{"rgba-cubemap-array"} + data.extension))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + Containers::Optional image = importer->image3D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + + CORRADE_COMPARE(image->size(), (Vector3i{27, 27, 12})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + + /* There are moderately significant compression artifacts */ + CORRADE_COMPARE_WITH(image->pixels()[0], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 10.83f})); + CORRADE_COMPARE_WITH(image->pixels()[1], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.972f})); + CORRADE_COMPARE_WITH(image->pixels()[2], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 10.591f})); + CORRADE_COMPARE_WITH(image->pixels()[3], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 10.83f})); + CORRADE_COMPARE_WITH(image->pixels()[4], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.972f})); + CORRADE_COMPARE_WITH(image->pixels()[5], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 10.591f})); + + /* Second layer, 2nd and 3rd face are switched */ + CORRADE_COMPARE_WITH(image->pixels()[6], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 10.83f})); + CORRADE_COMPARE_WITH(image->pixels()[8], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.972f})); + CORRADE_COMPARE_WITH(image->pixels()[7], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 10.591f})); + CORRADE_COMPARE_WITH(image->pixels()[9], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27.png"), + (DebugTools::CompareImageToFile{_manager, 94.0f, 10.83f})); + CORRADE_COMPARE_WITH(image->pixels()[10], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice1.png"), + (DebugTools::CompareImageToFile{_manager, 74.0f, 6.972f})); + CORRADE_COMPARE_WITH(image->pixels()[11], + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-27x27-slice2.png"), + (DebugTools::CompareImageToFile{_manager, 88.0f, 10.591f})); +} + +void BasisImporterTest::videoSeeking() { + auto& data = VideoSeekingData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + data.file))); + + CORRADE_COMPARE(importer->image2DCount(), 3); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->image2D(2)); + CORRADE_VERIFY(importer->image2D(0)); + CORRADE_VERIFY(!importer->image2D(2)); + CORRADE_VERIFY(importer->image2D(1)); + CORRADE_VERIFY(importer->image2D(2)); + CORRADE_VERIFY(importer->image2D(0)); + + CORRADE_COMPARE(out.str(), + "Trade::BasisImporter::image2D(): video frames must be transcoded sequentially, expected frame 0 but got 2\n" + "Trade::BasisImporter::image2D(): video frames must be transcoded sequentially, expected frame 1 or 0 but got 2\n"); +} + +void BasisImporterTest::videoVerbose() { + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + + std::ostringstream out; + Debug redirectDebug{&out}; + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + "rgba-video.basis"))); + CORRADE_COMPARE(out.str(), ""); + + importer->close(); + importer->setFlags(ImporterFlag::Verbose); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(BASISIMPORTER_TEST_DIR, + "rgba-video.basis"))); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::openData(): file contains video frames, images must be transcoded sequentially\n"); } void BasisImporterTest::openMemory() { @@ -497,7 +1191,7 @@ void BasisImporterTest::openMemory() { Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); CORRADE_VERIFY(!image->isCompressed()); - CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Unorm); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) @@ -508,7 +1202,7 @@ void BasisImporterTest::openMemory() { CORRADE_COMPARE_WITH(image->pixels(), Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 78.3f, 8.31f})); + (DebugTools::CompareImageToFile{_manager, 94.0f, 8.039f})); } void BasisImporterTest::openSameTwice() { @@ -521,23 +1215,25 @@ void BasisImporterTest::openSameTwice() { /* Shouldn't crash, leak or anything */ Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); - CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGBA8Unorm); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); } void BasisImporterTest::openDifferent() { Containers::Pointer importer = _manager.instantiate("BasisImporterEtc2RGBA"); + CORRADE_VERIFY(importer->openFile( - Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgb.basis"))); + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-video.basis"))); CORRADE_VERIFY(importer->openFile( - Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-2images-mips.basis"))); - CORRADE_COMPARE(importer->image2DCount(), 2); + Utility::Directory::join(BASISIMPORTER_TEST_DIR, "rgba-cubemap-array.ktx2"))); + CORRADE_COMPARE(importer->image3DCount(), 1); - /* Shouldn't crash, leak or anything */ - Containers::Optional image = importer->image2D(1); + /* Verify that everything is working properly with different files + and transcoders. Shouldn't crash, leak or anything. */ + Containers::Optional image = importer->image3D(0); CORRADE_VERIFY(image); - CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGBA8Unorm); - CORRADE_COMPARE(image->size(), (Vector2i{27, 63})); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGBA8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{27, 27, 12})); } void BasisImporterTest::importMultipleFormats() { @@ -550,14 +1246,14 @@ void BasisImporterTest::importMultipleFormats() { Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); - CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGBA8Unorm); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); } { importer->configuration().setValue("format", "Bc1RGB"); Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); - CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Bc1RGBUnorm); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Bc1RGBSrgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); } } diff --git a/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt b/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt index 3de72708c..63a39a570 100644 --- a/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt @@ -30,8 +30,10 @@ find_package(Magnum REQUIRED DebugTools) find_package(Magnum COMPONENTS AnyImageImporter) if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(KTXIMPORTER_TEST_DIR ".") set(BASISIMPORTER_TEST_DIR ".") else() + set(KTXIMPORTER_TEST_DIR ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test) set(BASISIMPORTER_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) endif() @@ -55,10 +57,53 @@ file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h corrade_add_test(BasisImporterTest BasisImporterTest.cpp LIBRARIES Magnum::Trade Magnum::DebugTools FILES - rgb.basis rgb-pow2.basis rgb-noflip.basis - rgba.basis rgba-pow2.basis rgba-2images-mips.basis - rgb-63x27.png rgba-63x27.png rgba-31x13.png rgba-15x6.png - rgba-27x63.png) + invalid-cube-face-count.basis + invalid-cube-face-size.basis + rgb.basis + rgb.ktx2 + rgb-pow2.basis + rgb-pow2.ktx2 + rgb-linear.basis + rgb-linear.ktx2 + rgb-linear-pow2.basis + rgb-linear-pow2.ktx2 + rgb-noflip.basis + rgb-noflip.ktx2 + rgba.basis + rgba.ktx2 + rgba-pow2.basis + rgba-pow2.ktx2 + rgba-uastc.basis + rgba-uastc.ktx2 + rgba-2images-mips.basis + rgba-3d.basis + rgba-3d.ktx2 + rgba-3d-mips.basis + rgba-3d-mips.ktx2 + rgba-array.basis + rgba-array.ktx2 + rgba-array-mips.basis + rgba-array-mips.ktx2 + rgba-cubemap.basis + rgba-cubemap.ktx2 + rgba-cubemap-array.basis + rgba-cubemap-array.ktx2 + rgba-video.basis + rgba-video.ktx2 + rgba-video-uastc.basis + rgba-video-uastc.ktx2 + rgb-63x27.png + rgba-63x27.png + rgba-63x27-slice1.png + rgba-63x27-slice2.png + rgba-31x13.png + rgba-13x31.png + rgba-15x6.png + rgba-6x15.png + rgba-27x27.png + rgba-27x27-slice1.png + rgba-27x27-slice2.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-rgba.ktx2) target_include_directories(BasisImporterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) if(MAGNUM_BASISIMPORTER_BUILD_STATIC) target_link_libraries(BasisImporterTest PRIVATE BasisImporter) diff --git a/src/MagnumPlugins/BasisImporter/Test/README.md b/src/MagnumPlugins/BasisImporter/Test/README.md index 8e14e08d5..cc6d95cf8 100644 --- a/src/MagnumPlugins/BasisImporter/Test/README.md +++ b/src/MagnumPlugins/BasisImporter/Test/README.md @@ -1,37 +1,50 @@ -Creating input Basis files -========================== +Creating input files +==================== -The images were converted from central cutouts from `ambient-texture.tga` and `diffuse-alpha-texture.tga` -from the [Magnum Shaders test files](https://github.com/mosra/magnum/tree/master/src/Magnum/Shaders/Test/TestFiles). +The images were converted from central cutouts from `ambient-texture.tga` +and `diffuse-alpha-texture.tga` from the [Magnum Shaders test files](https://github.com/mosra/magnum/tree/master/src/Magnum/Shaders/Test/TestFiles): - `rgb-63x27.png` - `rgb-64x32.png` -- `rgba-27x63.png` - `rgba-63x27.png` - `rgba-64x32.png` +- `rgba-27x27.png` -using the official basis universal -[conversion tool](https://github.com/BinomialLLC/basis_universal/#command-line-compression-tool). - -To convert, run the following commands: +`*-slice*.png` files were generated by h-flipping and inverting color channels: ```sh -basisu rgb-63x27.png -output_file rgb.basis -y_flip -basisu rgb-63x27.png -output_file rgb-noflip.basis -basisu rgba-63x27.png -output_file rgba.basis -force_alpha -y_flip -basisu rgba-63x27.png rgba-27x63.png -output_file rgba-2images-mips.basis -y_flip -mipmap -mip_smallest 16 -mip_filter box - -# Required for PVRTC1 target, which requires pow2 dimensions -basisu rgb-64x32.png -output_file rgb-pow2.basis -y_flip -basisu rgba-64x32.png -output_file rgba-pow2.basis -force_alpha -y_flip +convert -flop rgba-63x27.png rgba-63x27-slice1.png +convert -flop rgba-27x27.png rgba-27x27-slice1.png +convert -negate -channel RGB rgba-63x27.png rgba-63x27-slice2.png +convert -negate -channel RGB rgba-27x27.png rgba-27x27-slice2.png +pngcrush -ow rgba-63x27-slice1.png +pngcrush -ow rgba-27x27-slice1.png +pngcrush -ow rgba-63x27-slice2.png +pngcrush -ow rgba-27x27-slice2.png ``` +They are converted using the official basis universal +[conversion tool](https://github.com/BinomialLLC/basis_universal/#command-line-compression-tool). +For `*.ktx2` files at least version 1.15 is required. + +To convert to all the required `*.basis`/`*.ktx2` files, run the `convert.sh` script. + +Creating mipmaps +================ + For mipmap testing, the PNG image is resized to two more levels. Using the box filter, the same as Basis itself, to have the least difference. ```sh convert rgba-63x27.png -filter box -resize 31x13\! rgba-31x13.png convert rgba-63x27.png -filter box -resize 15x6\! rgba-15x6.png +convert rgba-31x13.png -rotate 90 rgba-13x31.png +convert rgba-15x6.png -rotate 90 rgba-6x15.png + pngcrush -ow rgba-31x13.png pngcrush -ow rgba-15x6.png +pngcrush -ow rgba-13x31.png +# For whatever reason, this removes the alpha channel +# 'libpng warning: Invalid number of transparent colors specified' +#pngcrush -ow rgba-6x15.png ``` diff --git a/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake b/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake index cd22a3cc4..caa4fe4e0 100644 --- a/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake +++ b/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake @@ -26,4 +26,5 @@ #cmakedefine BASISIMPORTER_PLUGIN_FILENAME "${BASISIMPORTER_PLUGIN_FILENAME}" #cmakedefine STBIMAGEIMPORTER_PLUGIN_FILENAME "${STBIMAGEIMPORTER_PLUGIN_FILENAME}" +#define KTXIMPORTER_TEST_DIR "${KTXIMPORTER_TEST_DIR}" #define BASISIMPORTER_TEST_DIR "${BASISIMPORTER_TEST_DIR}" diff --git a/src/MagnumPlugins/BasisImporter/Test/convert.sh b/src/MagnumPlugins/BasisImporter/Test/convert.sh new file mode 100644 index 000000000..c98aa8ea1 --- /dev/null +++ b/src/MagnumPlugins/BasisImporter/Test/convert.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +set -e + +# RGB +basisu rgb-63x27.png -output_file rgb.basis -y_flip +basisu rgb-63x27.png -output_file rgb.ktx2 -y_flip -ktx2 +# basisu doesn't write KTXorientation metadata in KTX2 but that's the only way +# for the plugin to detect y-flip on import. Patch it in manually for testing. +sed -b 's/\x1f\x00\x00\x00KTXwriter\x00Basis Universal /\x12\x00\x00\x00KTXorientation\x00ru\x00\x00\x00\x07\x00\x00\x00_\x00/' rgb.ktx2 > rgb.ktx2.tmp +mv rgb.ktx2.tmp rgb.ktx2 +# Without y-flip +basisu rgb-63x27.png -output_file rgb-noflip.basis +basisu rgb-63x27.png -output_file rgb-noflip.ktx2 -ktx2 +# Linear image data +basisu rgb-63x27.png -output_file rgb-linear.basis -y_flip -linear +basisu rgb-63x27.png -output_file rgb-linear.ktx2 -y_flip -linear -ktx2 + +# RGBA +basisu rgba-63x27.png -output_file rgba.basis -force_alpha -y_flip +basisu rgba-63x27.png -output_file rgba.ktx2 -force_alpha -y_flip -ktx2 + +# UASTC +basisu rgba-63x27.png -output_file rgba-uastc.basis -uastc -force_alpha -y_flip +basisu rgba-63x27.png -output_file rgba-uastc.ktx2 -uastc -force_alpha -y_flip -ktx2 + +# Multiple images, not possible with KTX2 +basisu rgba-63x27.png rgba-13x31.png -output_file rgba-2images-mips.basis -y_flip -mipmap -mip_smallest 16 -mip_filter box + +# 2D array +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 2darray -output_file rgba-array.basis -force_alpha -y_flip +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 2darray -output_file rgba-array.ktx2 -force_alpha -y_flip -ktx2 + +# 2D array with mipmaps +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 2darray -output_file rgba-array-mips.basis -force_alpha -y_flip -mipmap -mip_smallest 16 -mip_filter box +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 2darray -output_file rgba-array-mips.ktx2 -force_alpha -y_flip -mipmap -mip_smallest 16 -mip_filter box -ktx2 + +# Video +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type video -output_file rgba-video.basis -force_alpha -y_flip +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type video -output_file rgba-video.ktx2 -force_alpha -y_flip -ktx2 + +# Video with UASTC to make sure the I-frame detection works +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type video -output_file rgba-video-uastc.basis -uastc -force_alpha -y_flip +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type video -output_file rgba-video-uastc.ktx2 -uastc -force_alpha -y_flip -ktx2 + +# 3D +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 3d -output_file rgba-3d.basis -force_alpha -y_flip +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 3d -output_file rgba-3d.ktx2 -force_alpha -y_flip -ktx2 + +# 3D with mipmaps +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 3d -output_file rgba-3d-mips.basis -force_alpha -y_flip -mipmap -mip_smallest 16 -mip_filter box +basisu rgba-63x27.png rgba-63x27-slice1.png rgba-63x27-slice2.png -tex_type 3d -output_file rgba-3d-mips.ktx2 -force_alpha -y_flip -mipmap -mip_smallest 16 -mip_filter box -ktx2 + +# Cube map +basisu rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png -tex_type cubemap -output_file rgba-cubemap.basis -force_alpha -y_flip +basisu rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png -tex_type cubemap -output_file rgba-cubemap.ktx2 -force_alpha -y_flip -ktx2 + +# Cube map array +# Second layer has the 2nd and 3rd face switched +basisu rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png rgba-27x27.png rgba-27x27-slice2.png rgba-27x27-slice1.png rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png -tex_type cubemap -output_file rgba-cubemap-array.basis -force_alpha -y_flip +basisu rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png rgba-27x27.png rgba-27x27-slice2.png rgba-27x27-slice1.png rgba-27x27.png rgba-27x27-slice1.png rgba-27x27-slice2.png -tex_type cubemap -output_file rgba-cubemap-array.ktx2 -force_alpha -y_flip -ktx2 + +# Invalid files. These can't be patched at runtime because of CRC checks on the +# header. +# Cube map faces are not square. basisu allows this, BasisImporter does not +# (to match KTX2 behaviour). +basisu rgba-15x6.png rgba-15x6.png rgba-15x6.png rgba-15x6.png rgba-15x6.png rgba-15x6.png -tex_type cubemap -output_file invalid-cube-face-size.basis +# We can't generate cube maps with non-multiples-of-6 face counts, so for +# invalid-cube-face-count.basis we have to copy rgba-array.basis, patch +# m_tex_type in the header to CubemapArray and update the CRC16 manually. +cp rgba-array.basis invalid-cube-face-count.basis +printf '\x02' | dd conv=notrunc of=invalid-cube-face-count.basis bs=1 seek=23 +printf '\x2c\xb0' | dd conv=notrunc of=invalid-cube-face-count.basis bs=1 seek=6 + +# Required for PVRTC1 target, which requires pow2 dimensions +basisu rgb-64x32.png -output_file rgb-pow2.basis -y_flip +basisu rgb-64x32.png -output_file rgb-pow2.ktx2 -y_flip -ktx2 +basisu rgb-64x32.png -output_file rgb-linear-pow2.basis -y_flip -linear +basisu rgb-64x32.png -output_file rgb-linear-pow2.ktx2 -y_flip -linear -ktx2 +basisu rgba-64x32.png -output_file rgba-pow2.basis -force_alpha -y_flip +basisu rgba-64x32.png -output_file rgba-pow2.ktx2 -force_alpha -y_flip -ktx2 diff --git a/src/MagnumPlugins/BasisImporter/Test/invalid-cube-face-count.basis b/src/MagnumPlugins/BasisImporter/Test/invalid-cube-face-count.basis new file mode 100644 index 000000000..82f97080a Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/invalid-cube-face-count.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/invalid-cube-face-size.basis b/src/MagnumPlugins/BasisImporter/Test/invalid-cube-face-size.basis new file mode 100644 index 000000000..2b10128ba Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/invalid-cube-face-size.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-31x13.png b/src/MagnumPlugins/BasisImporter/Test/rgb-31x13.png deleted file mode 100644 index 91a76922f..000000000 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgb-31x13.png and /dev/null differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-linear-pow2.basis b/src/MagnumPlugins/BasisImporter/Test/rgb-linear-pow2.basis new file mode 100644 index 000000000..ce6b758a7 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb-linear-pow2.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-linear-pow2.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgb-linear-pow2.ktx2 new file mode 100644 index 000000000..4f53115cb Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb-linear-pow2.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-linear.basis b/src/MagnumPlugins/BasisImporter/Test/rgb-linear.basis new file mode 100644 index 000000000..b554c753b Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb-linear.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-linear.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgb-linear.ktx2 new file mode 100644 index 000000000..251171c70 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb-linear.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.basis b/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.basis index 77abeae65..ccbe1c609 100644 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.basis and b/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.ktx2 new file mode 100644 index 000000000..7709f6618 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb-noflip.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.basis b/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.basis index 2499c3b58..8dd386a18 100644 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.basis and b/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.ktx2 new file mode 100644 index 000000000..9dd72038c Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb-pow2.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb.basis b/src/MagnumPlugins/BasisImporter/Test/rgb.basis index 010f5d2e0..4ddac50d2 100644 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgb.basis and b/src/MagnumPlugins/BasisImporter/Test/rgb.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgb.ktx2 new file mode 100644 index 000000000..41cc6ed21 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgb.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-13x31.png b/src/MagnumPlugins/BasisImporter/Test/rgba-13x31.png new file mode 100644 index 000000000..04ef75084 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-13x31.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-27x27-slice1.png b/src/MagnumPlugins/BasisImporter/Test/rgba-27x27-slice1.png new file mode 100644 index 000000000..233fd992c Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-27x27-slice1.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-27x27-slice2.png b/src/MagnumPlugins/BasisImporter/Test/rgba-27x27-slice2.png new file mode 100644 index 000000000..785e8af17 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-27x27-slice2.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-27x27.png b/src/MagnumPlugins/BasisImporter/Test/rgba-27x27.png new file mode 100644 index 000000000..b9f70b15c Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-27x27.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-27x63.png b/src/MagnumPlugins/BasisImporter/Test/rgba-27x63.png deleted file mode 100644 index 6e0a1501f..000000000 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgba-27x63.png and /dev/null differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-2images-mips.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-2images-mips.basis index f3d7e398e..9fe10e4db 100644 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgba-2images-mips.basis and b/src/MagnumPlugins/BasisImporter/Test/rgba-2images-mips.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-3d-mips.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-3d-mips.basis new file mode 100644 index 000000000..ccb28883b Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-3d-mips.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-3d-mips.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-3d-mips.ktx2 new file mode 100644 index 000000000..b6c39c854 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-3d-mips.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-3d.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-3d.basis new file mode 100644 index 000000000..175658e46 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-3d.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-3d.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-3d.ktx2 new file mode 100644 index 000000000..5fbed6d1d Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-3d.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-63x27-slice1.png b/src/MagnumPlugins/BasisImporter/Test/rgba-63x27-slice1.png new file mode 100644 index 000000000..3604d0c58 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-63x27-slice1.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-63x27-slice2.png b/src/MagnumPlugins/BasisImporter/Test/rgba-63x27-slice2.png new file mode 100644 index 000000000..118ce434e Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-63x27-slice2.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-6x15.png b/src/MagnumPlugins/BasisImporter/Test/rgba-6x15.png new file mode 100644 index 000000000..57e929ded Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-6x15.png differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-array-mips.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-array-mips.basis new file mode 100644 index 000000000..11317c9f3 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-array-mips.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-array-mips.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-array-mips.ktx2 new file mode 100644 index 000000000..b6c39c854 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-array-mips.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-array.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-array.basis new file mode 100644 index 000000000..192f37f9a Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-array.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-array.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-array.ktx2 new file mode 100644 index 000000000..5fbed6d1d Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-array.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap-array.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap-array.basis new file mode 100644 index 000000000..006f2445d Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap-array.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap-array.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap-array.ktx2 new file mode 100644 index 000000000..808928bb8 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap-array.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap.basis new file mode 100644 index 000000000..1f33d6d65 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap.ktx2 new file mode 100644 index 000000000..a84339ae4 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-cubemap.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.basis index 74ef64035..7d85115be 100644 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.basis and b/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.ktx2 new file mode 100644 index 000000000..2eca388f2 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-pow2.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-uastc.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-uastc.basis new file mode 100644 index 000000000..f1d4d71f6 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-uastc.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-uastc.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-uastc.ktx2 new file mode 100644 index 000000000..2d79dbcf9 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-uastc.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-video-uastc.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-video-uastc.basis new file mode 100644 index 000000000..063ae2f35 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-video-uastc.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-video-uastc.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-video-uastc.ktx2 new file mode 100644 index 000000000..1d546f80a Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-video-uastc.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-video.basis b/src/MagnumPlugins/BasisImporter/Test/rgba-video.basis new file mode 100644 index 000000000..bb75249d2 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-video.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-video.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba-video.ktx2 new file mode 100644 index 000000000..f7dca7550 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba-video.ktx2 differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba.basis b/src/MagnumPlugins/BasisImporter/Test/rgba.basis index fbb3d23a6..0dc6786f4 100644 Binary files a/src/MagnumPlugins/BasisImporter/Test/rgba.basis and b/src/MagnumPlugins/BasisImporter/Test/rgba.basis differ diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgba.ktx2 new file mode 100644 index 000000000..a45226961 Binary files /dev/null and b/src/MagnumPlugins/BasisImporter/Test/rgba.ktx2 differ