diff --git a/CMakeLists.txt b/CMakeLists.txt index 795e1409b..2c509db62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,8 @@ option(WITH_HARFBUZZFONT "Build HarfBuzzFont plugin" OFF) option(WITH_ICOIMPORTER "Build IcoImporter plugin" OFF) option(WITH_JPEGIMAGECONVERTER "Build JpegImageConverter plugin" OFF) option(WITH_JPEGIMPORTER "Build JpegImporter plugin" OFF) +option(WITH_KTXIMAGECONVERTER "Build KtxImageConverter plugin" OFF) +option(WITH_KTXIMPORTER "Build KtxImporter plugin" OFF) option(WITH_MESHOPTIMIZERSCENECONVERTER "Build MeshOptimizerSceneConverter plugin" OFF) option(WITH_MINIEXRIMAGECONVERTER "Build MiniExrImageConverter plugin" OFF) cmake_dependent_option(WITH_OPENDDL "Build OpenDdl library" OFF "NOT WITH_OPENGEXIMPORTER" ON) diff --git a/doc/building-plugins.dox b/doc/building-plugins.dox index b58d68bc4..fcf5c0a7e 100644 --- a/doc/building-plugins.dox +++ b/doc/building-plugins.dox @@ -369,6 +369,10 @@ By default no plugins are built and you need to select them manually: @ref Trade::JpegImageConverter "JpegImageConverter" plugin. - `WITH_JPEGIMPORTER` --- Build the @ref Trade::JpegImporter "JpegImporter" plugin. Depends on [libJPEG](http://libjpeg.sourceforge.net/). +- `WITH_KTXIMAGECONVERTER` --- Build the + @relativeref{Trade,KtxImageConverter} plugin. +- `WITH_KTXIMPORTER` --- Build the + @relativeref{Trade,KtxImporter} plugin. - `WITH_MESHOPTIMIZERSCENECONVERTER` --- Build the @ref Trade::MeshOptimizerSceneConverter "MeshOptimizerSceneConverter" plugin. diff --git a/doc/cmake-plugins.dox b/doc/cmake-plugins.dox index b364d8d30..b2c50b1da 100644 --- a/doc/cmake-plugins.dox +++ b/doc/cmake-plugins.dox @@ -140,6 +140,9 @@ This command will not try to find any actual plugin. The plugins are: - `JpegImageConverter` --- @ref Trade::JpegImageConverter "JpegImageConverter" plugin - `JpegImporter` --- @ref Trade::JpegImporter "JpegImporter" plugin +- `KtxImageConverter` --- @ref Trade::KtxImageConverter "KtxImageConverter" + plugin +- `KtxImporter` --- @ref Trade::KtxImporter "KtxImporter" plugin - `MeshOptimizerSceneConverter` --- @ref Trade::MeshOptimizerSceneConverter "MeshOptimizerSceneConverter" plugin diff --git a/doc/namespaces.dox b/doc/namespaces.dox index dd1c8be65..9f4c4cf99 100644 --- a/doc/namespaces.dox +++ b/doc/namespaces.dox @@ -76,6 +76,12 @@ /** @dir MagnumPlugins/JpegImporter * @brief Plugin @ref Magnum::Trade::JpegImporter */ +/** @dir MagnumPlugins/KtxImageConverter + * @brief Plugin @ref Magnum::Trade::KtxImageConverter + */ +/** @dir MagnumPlugins/KtxImporter + * @brief Plugin @ref Magnum::Trade::KtxImporter + */ /** @dir MagnumPlugins/MeshOptimizerSceneConverter * @brief Plugin @ref Magnum::Trade::MeshOptimizerSceneConverter * @m_since_{plugins,2020,06} diff --git a/modules/FindMagnumPlugins.cmake b/modules/FindMagnumPlugins.cmake index 99a265c9e..a5ed7ab9c 100644 --- a/modules/FindMagnumPlugins.cmake +++ b/modules/FindMagnumPlugins.cmake @@ -27,6 +27,8 @@ # IcoImporter - ICO importer # JpegImageConverter - JPEG image converter # JpegImporter - JPEG importer +# KtxImageConverter - KTX image converter +# KtxImporter - KTX importer # MeshOptimizerSceneConverter - MeshOptimizer scene converter # MiniExrImageConverter - OpenEXR image converter using miniexr # OpenGexImporter - OpenGEX importer @@ -144,11 +146,12 @@ set(_MAGNUMPLUGINS_PLUGIN_COMPONENTS DevIlImageImporter DrFlacAudioImporter DrMp3AudioImporter DrWavAudioImporter Faad2AudioImporter FreeTypeFont GlslangShaderConverter HarfBuzzFont IcoImporter JpegImageConverter JpegImporter - MeshOptimizerSceneConverter MiniExrImageConverter OpenExrImageConverter - OpenExrImporter OpenGexImporter PngImageConverter PngImporter - PrimitiveImporter SpirvToolsShaderConverter StanfordImporter - StanfordSceneConverter StbDxtImageConverter StbImageConverter - StbImageImporter StbTrueTypeFont StbVorbisAudioImporter StlImporter + KtxImageConverter KtxImporter MeshOptimizerSceneConverter + MiniExrImageConverter OpenExrImageConverter OpenExrImporter + OpenGexImporter PngImageConverter PngImporter PrimitiveImporter + SpirvToolsShaderConverter StanfordImporter StanfordSceneConverter + StbDxtImageConverter StbImageConverter StbImageImporter + StbTrueTypeFont StbVorbisAudioImporter StlImporter TinyGltfImporter) # Nothing is enabled by default right now set(_MAGNUMPLUGINS_IMPLICITLY_ENABLED_COMPONENTS ) @@ -384,6 +387,9 @@ foreach(_component ${MagnumPlugins_FIND_COMPONENTS}) INTERFACE_LINK_LIBRARIES ${JPEG_LIBRARIES}) endif() + # KtxImageConverter has no dependencies + # KtxImporter has no dependencies + # MeshOptimizerSceneConverter plugin dependencies elseif(_component STREQUAL MeshOptimizerSceneConverter) if(NOT TARGET meshoptimizer) diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 11193fcd8..18e031526 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -38,6 +38,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-android-arm64 b/package/archlinux/PKGBUILD-android-arm64 index c81226a84..a3585de91 100644 --- a/package/archlinux/PKGBUILD-android-arm64 +++ b/package/archlinux/PKGBUILD-android-arm64 @@ -46,6 +46,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=OFF \ -DWITH_JPEGIMPORTER=OFF \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/archlinux/PKGBUILD-clang b/package/archlinux/PKGBUILD-clang index 5e2fcaacc..582c4c217 100644 --- a/package/archlinux/PKGBUILD-clang +++ b/package/archlinux/PKGBUILD-clang @@ -50,6 +50,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-addresssanitizer b/package/archlinux/PKGBUILD-clang-addresssanitizer index 38d42bb6d..57d290074 100644 --- a/package/archlinux/PKGBUILD-clang-addresssanitizer +++ b/package/archlinux/PKGBUILD-clang-addresssanitizer @@ -40,6 +40,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-clang-threadsanitizer b/package/archlinux/PKGBUILD-clang-threadsanitizer index 9a8ea8a4e..fa8ce7631 100644 --- a/package/archlinux/PKGBUILD-clang-threadsanitizer +++ b/package/archlinux/PKGBUILD-clang-threadsanitizer @@ -40,6 +40,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-coverage b/package/archlinux/PKGBUILD-coverage index c4a358e8a..dcc3bd857 100644 --- a/package/archlinux/PKGBUILD-coverage +++ b/package/archlinux/PKGBUILD-coverage @@ -43,6 +43,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-emscripten b/package/archlinux/PKGBUILD-emscripten index 2603eb2a0..f8751afc3 100644 --- a/package/archlinux/PKGBUILD-emscripten +++ b/package/archlinux/PKGBUILD-emscripten @@ -42,6 +42,8 @@ build() { -DWITH_DRWAVAUDIOIMPORTER=ON \ -DWITH_FAAD2AUDIOIMPORTER=ON \ -DWITH_ICOIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/archlinux/PKGBUILD-emscripten-wasm b/package/archlinux/PKGBUILD-emscripten-wasm index 619aeb00a..3743bfb3f 100644 --- a/package/archlinux/PKGBUILD-emscripten-wasm +++ b/package/archlinux/PKGBUILD-emscripten-wasm @@ -41,6 +41,8 @@ build() { -DWITH_DRWAVAUDIOIMPORTER=ON \ -DWITH_FAAD2AUDIOIMPORTER=ON \ -DWITH_ICOIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 b/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 index c7b91f8cb..cd224c540 100644 --- a/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 +++ b/package/archlinux/PKGBUILD-emscripten-wasm-webgl2 @@ -41,6 +41,8 @@ build() { -DWITH_DRWAVAUDIOIMPORTER=ON \ -DWITH_FAAD2AUDIOIMPORTER=ON \ -DWITH_ICOIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/archlinux/PKGBUILD-gcc48 b/package/archlinux/PKGBUILD-gcc48 index e041e3075..8484eea6b 100644 --- a/package/archlinux/PKGBUILD-gcc48 +++ b/package/archlinux/PKGBUILD-gcc48 @@ -50,6 +50,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/PKGBUILD-mingw-w64 b/package/archlinux/PKGBUILD-mingw-w64 index ef6a8bc2f..73f4926b2 100644 --- a/package/archlinux/PKGBUILD-mingw-w64 +++ b/package/archlinux/PKGBUILD-mingw-w64 @@ -35,6 +35,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ @@ -82,6 +84,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/archlinux/PKGBUILD-release b/package/archlinux/PKGBUILD-release index 48f802d11..7b1e4e5cf 100644 --- a/package/archlinux/PKGBUILD-release +++ b/package/archlinux/PKGBUILD-release @@ -38,6 +38,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ @@ -82,6 +84,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/magnum-plugins-git/PKGBUILD b/package/archlinux/magnum-plugins-git/PKGBUILD index 68fff3f80..7876abe25 100644 --- a/package/archlinux/magnum-plugins-git/PKGBUILD +++ b/package/archlinux/magnum-plugins-git/PKGBUILD @@ -47,6 +47,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/archlinux/magnum-plugins/PKGBUILD b/package/archlinux/magnum-plugins/PKGBUILD index 07d8cf95f..ade70ec46 100644 --- a/package/archlinux/magnum-plugins/PKGBUILD +++ b/package/archlinux/magnum-plugins/PKGBUILD @@ -50,6 +50,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/ci/appveyor-desktop-mingw.bat b/package/ci/appveyor-desktop-mingw.bat index c4b596eb6..37bd98f48 100644 --- a/package/ci/appveyor-desktop-mingw.bat +++ b/package/ci/appveyor-desktop-mingw.bat @@ -121,6 +121,8 @@ cmake .. ^ -DWITH_ICOIMPORTER=ON ^ -DWITH_JPEGIMAGECONVERTER=ON ^ -DWITH_JPEGIMPORTER=ON ^ + -DWITH_KTXIMAGECONVERTER=ON ^ + -DWITH_KTXIMPORTER=ON ^ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON ^ -DWITH_MINIEXRIMAGECONVERTER=ON ^ -DWITH_OPENEXRIMAGECONVERTER=ON ^ diff --git a/package/ci/appveyor-desktop.bat b/package/ci/appveyor-desktop.bat index a7b4d572d..ff0fd4573 100644 --- a/package/ci/appveyor-desktop.bat +++ b/package/ci/appveyor-desktop.bat @@ -99,6 +99,8 @@ cmake .. ^ -DWITH_ICOIMPORTER=ON ^ -DWITH_JPEGIMAGECONVERTER=%EXCEPT_MSVC2015% ^ -DWITH_JPEGIMPORTER=%EXCEPT_MSVC2015% ^ + -DWITH_KTXIMAGECONVERTER=ON ^ + -DWITH_KTXIMPORTER=ON ^ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON ^ -DWITH_MINIEXRIMAGECONVERTER=ON ^ -DWITH_OPENEXRIMAGECONVERTER=%EXCEPT_MSVC2015% ^ diff --git a/package/ci/appveyor-rt.bat b/package/ci/appveyor-rt.bat index 1d5ed51a1..a0a856013 100644 --- a/package/ci/appveyor-rt.bat +++ b/package/ci/appveyor-rt.bat @@ -78,6 +78,8 @@ cmake .. ^ -DWITH_ICOIMPORTER=ON ^ -DWITH_JPEGIMAGECONVERTER=OFF ^ -DWITH_JPEGIMPORTER=OFF ^ + -DWITH_KTXIMAGECONVERTER=ON ^ + -DWITH_KTXIMPORTER=ON ^ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF ^ -DWITH_MINIEXRIMAGECONVERTER=ON ^ -DWITH_OPENEXRIMAGECONVERTER=OFF ^ diff --git a/package/ci/emscripten.sh b/package/ci/emscripten.sh index dd315ad82..e0e6fd5d4 100755 --- a/package/ci/emscripten.sh +++ b/package/ci/emscripten.sh @@ -97,6 +97,8 @@ cmake .. \ -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=OFF \ -DWITH_JPEGIMPORTER=OFF \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/ci/travis-android-arm.sh b/package/ci/travis-android-arm.sh index 821078526..6dcb26d7f 100755 --- a/package/ci/travis-android-arm.sh +++ b/package/ci/travis-android-arm.sh @@ -91,6 +91,8 @@ cmake .. \ -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=OFF \ -DWITH_JPEGIMPORTER=OFF \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/ci/travis-ios-simulator.sh b/package/ci/travis-ios-simulator.sh index 9fbcf4e51..a243ca900 100755 --- a/package/ci/travis-ios-simulator.sh +++ b/package/ci/travis-ios-simulator.sh @@ -83,6 +83,8 @@ cmake .. \ -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=OFF \ -DWITH_JPEGIMPORTER=OFF \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=OFF \ diff --git a/package/ci/unix-desktop.sh b/package/ci/unix-desktop.sh index 633b468fa..bacf81407 100755 --- a/package/ci/unix-desktop.sh +++ b/package/ci/unix-desktop.sh @@ -67,6 +67,8 @@ cmake .. \ -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=ON \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/debian/rules b/package/debian/rules index edbd9fd01..c2c308ace 100755 --- a/package/debian/rules +++ b/package/debian/rules @@ -29,6 +29,8 @@ override_dh_auto_configure: -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild b/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild index df460fcfb..86a994c8d 100644 --- a/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild +++ b/package/gentoo/dev-libs/magnum-plugins/magnum-plugins-9999.ebuild @@ -47,6 +47,8 @@ src_configure() { -DWITH_ICOIMPORTER=ON -DWITH_JPEGIMAGECONVERTER=ON -DWITH_JPEGIMPORTER=ON + -DWITH_KTXIMAGECONVERTER=ON + -DWITH_KTXIMPORTER=ON -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF -DWITH_MINIEXRIMAGECONVERTER=ON -DWITH_OPENEXRIMAGECONVERTER=ON diff --git a/package/homebrew/magnum-plugins.rb b/package/homebrew/magnum-plugins.rb index 38e167d64..4e9b7f11d 100644 --- a/package/homebrew/magnum-plugins.rb +++ b/package/homebrew/magnum-plugins.rb @@ -64,6 +64,8 @@ def install "-DWITH_HARFBUZZFONT=#{(build.with? 'harfbuzz') ? 'ON' : 'OFF'}", "-DWITH_JPEGIMAGECONVERTER=#{(build.with? 'jpeg') ? 'ON' : 'OFF'}", "-DWITH_JPEGIMPORTER=#{(build.with? 'jpeg') ? 'ON' : 'OFF'}", + "-DWITH_KTXIMAGECONVERTER=ON", + "-DWITH_KTXIMAGEIMPORTER=ON", "-DWITH_MESHOPTIMIZERSCENECONVERTER=ON", "-DWITH_MINIEXRIMAGECONVERTER=ON", "-DWITH_OPENEXRIMAGECONVERTER=#{(build.with? 'openexr') ? 'ON' : 'OFF'}", diff --git a/package/msys/PKGBUILD b/package/msys/PKGBUILD index d75f14b9c..f85712257 100644 --- a/package/msys/PKGBUILD +++ b/package/msys/PKGBUILD @@ -51,6 +51,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/package/msys/magnum-plugins/PKGBUILD b/package/msys/magnum-plugins/PKGBUILD index 1ef5a0d22..0d2c688dd 100644 --- a/package/msys/magnum-plugins/PKGBUILD +++ b/package/msys/magnum-plugins/PKGBUILD @@ -58,6 +58,8 @@ build() { -DWITH_ICOIMPORTER=ON \ -DWITH_JPEGIMAGECONVERTER=ON \ -DWITH_JPEGIMPORTER=ON \ + -DWITH_KTXIMAGECONVERTER=ON \ + -DWITH_KTXIMPORTER=ON \ -DWITH_MESHOPTIMIZERSCENECONVERTER=OFF \ -DWITH_MINIEXRIMAGECONVERTER=ON \ -DWITH_OPENEXRIMAGECONVERTER=ON \ diff --git a/src/MagnumPlugins/CMakeLists.txt b/src/MagnumPlugins/CMakeLists.txt index fe4f5da87..dcf8ba1c0 100644 --- a/src/MagnumPlugins/CMakeLists.txt +++ b/src/MagnumPlugins/CMakeLists.txt @@ -95,6 +95,14 @@ if(WITH_JPEGIMPORTER) add_subdirectory(JpegImporter) endif() +if(WITH_KTXIMAGECONVERTER) + add_subdirectory(KtxImageConverter) +endif() + +if(WITH_KTXIMPORTER) + add_subdirectory(KtxImporter) +endif() + if(WITH_MESHOPTIMIZERSCENECONVERTER) add_subdirectory(MeshOptimizerSceneConverter) endif() diff --git a/src/MagnumPlugins/KtxImageConverter/CMakeLists.txt b/src/MagnumPlugins/KtxImageConverter/CMakeLists.txt new file mode 100644 index 000000000..3b2b35281 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/CMakeLists.txt @@ -0,0 +1,72 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# 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"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +find_package(Magnum REQUIRED Trade) + +if(BUILD_PLUGINS_STATIC AND NOT DEFINED MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC) + set(MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC 1) +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + +# KtxImageConverter plugin +add_plugin(KtxImageConverter + "${MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_IMAGECONVERTER_DEBUG_LIBRARY_INSTALL_DIR}" + "${MAGNUM_PLUGINS_IMAGECONVERTER_RELEASE_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_IMAGECONVERTER_RELEASE_LIBRARY_INSTALL_DIR}" + KtxImageConverter.conf + KtxImageConverter.cpp + KtxImageConverter.h) +if(MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC AND BUILD_STATIC_PIC) + set_target_properties(KtxImageConverter PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif() +target_include_directories(KtxImageConverter PUBLIC + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_BINARY_DIR}/src) +target_link_libraries(KtxImageConverter PUBLIC Magnum::Trade) +# Modify output location only if all are set, otherwise it makes no sense +if(CMAKE_RUNTIME_OUTPUT_DIRECTORY AND CMAKE_LIBRARY_OUTPUT_DIRECTORY AND CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set_target_properties(KtxImageConverter PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/magnum$<$:-d>/imageconverters + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/magnum$<$:-d>/imageconverters + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/magnum$<$:-d>/imageconverters) +endif() + +install(FILES KtxImageConverter.h ${CMAKE_CURRENT_BINARY_DIR}/configure.h + DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/KtxImageConverter) + +# Automatic static plugin import +if(MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC) + install(FILES importStaticPlugin.cpp DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/KtxImageConverter) + target_sources(KtxImageConverter INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/importStaticPlugin.cpp) +endif() + +if(BUILD_TESTS) + add_subdirectory(Test) +endif() + +# MagnumPlugins KtxImageConverter target alias for superprojects +add_library(MagnumPlugins::KtxImageConverter ALIAS KtxImageConverter) diff --git a/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.conf b/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.conf new file mode 100644 index 000000000..2d4ee3401 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.conf @@ -0,0 +1,15 @@ +# [configuration_] +[configuration] +# Orientation string to save in the file header. This doesn't flip the input +# pixels, it only tells readers the orientation during import. Must be empty +# or a string of the form [rl][du][oi] (r/l: right/left, d/u: down/up, +# o/i: out of/into the screen). Only subsets of rdi and ruo are recommended, other +# values may not be supported by all readers. +orientation=ruo +# Format swizzle string to save in the file header. This doesn't save swizzled +# data, it only tells readers the desired channel mapping during import. Must +# be empty or 4 characters long, valid characters are r,g,b,a,0,1. +swizzle= +# Name of the tool writing the image file, saved in the file header +writerName=Magnum KtxImageConverter +# [configuration_] diff --git a/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.cpp b/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.cpp new file mode 100644 index 000000000..01e08a5a9 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.cpp @@ -0,0 +1,943 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "KtxImageConverter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "MagnumPlugins/KtxImporter/KtxHeader.h" + +namespace Magnum { namespace Trade { + +namespace { + +/* Overloaded functions to use different pixel formats in templated code */ +bool isFormatImplementationSpecific(PixelFormat format) { + return isPixelFormatImplementationSpecific(format); +} + +bool isFormatImplementationSpecific(CompressedPixelFormat format) { + return isCompressedPixelFormatImplementationSpecific(format); +} + +typedef Containers::Pair FormatPair; + +FormatPair vulkanFormat(PixelFormat format) { + switch(format) { + #define _c(vulkan, magnum, type) case PixelFormat::magnum: \ + return {vulkan, Implementation::VkFormatSuffix::type}; + #include "MagnumPlugins/KtxImporter/formatMapping.hpp" + #undef _c + default: + return {{}, {}}; + } +} + +FormatPair vulkanFormat(CompressedPixelFormat format) { + /* In Vulkan there is no distinction between RGB and RGBA PVRTC: + https://github.com/KhronosGroup/Vulkan-Docs/issues/512#issuecomment-307768667 + compressedFormatMapping.hpp (generated from Vk::PixelFormat) contains the + RGBA variants, so we manually alias them here. We can't do this inside + compressedFormatMapping.hpp because both Magnum and Vulkan formats must + be unique for switch cases. */ + switch(format) { + case CompressedPixelFormat::PvrtcRGB2bppUnorm: + format = CompressedPixelFormat::PvrtcRGBA2bppUnorm; + break; + case CompressedPixelFormat::PvrtcRGB2bppSrgb: + format = CompressedPixelFormat::PvrtcRGBA2bppSrgb; + break; + case CompressedPixelFormat::PvrtcRGB4bppUnorm: + format = CompressedPixelFormat::PvrtcRGBA4bppUnorm; + break; + case CompressedPixelFormat::PvrtcRGB4bppSrgb: + format = CompressedPixelFormat::PvrtcRGBA4bppSrgb; + break; + default: + break; + } + + switch(format) { + #define _c(vulkan, magnum, type) case CompressedPixelFormat::magnum: \ + return {vulkan, Implementation::VkFormatSuffix::type}; + #include "MagnumPlugins/KtxImporter/compressedFormatMapping.hpp" + #undef _c + default: + return {{}, {}}; + } +} + +Vector3i formatUnitSize(PixelFormat) { + return {1, 1, 1}; +} + +Vector3i formatUnitSize(CompressedPixelFormat format) { + return compressedBlockSize(format); +} + +UnsignedInt formatUnitDataSize(PixelFormat format) { + return pixelSize(format); +} + +UnsignedInt formatUnitDataSize(CompressedPixelFormat format) { + return compressedBlockDataSize(format); +} + +UnsignedByte formatTypeSize(PixelFormat format) { + switch(format) { + case PixelFormat::R8Unorm: + case PixelFormat::RG8Unorm: + case PixelFormat::RGB8Unorm: + case PixelFormat::RGBA8Unorm: + case PixelFormat::R8Snorm: + case PixelFormat::RG8Snorm: + case PixelFormat::RGB8Snorm: + case PixelFormat::RGBA8Snorm: + case PixelFormat::R8Srgb: + case PixelFormat::RG8Srgb: + case PixelFormat::RGB8Srgb: + case PixelFormat::RGBA8Srgb: + case PixelFormat::R8UI: + case PixelFormat::RG8UI: + case PixelFormat::RGB8UI: + case PixelFormat::RGBA8UI: + case PixelFormat::R8I: + case PixelFormat::RG8I: + case PixelFormat::RGB8I: + case PixelFormat::RGBA8I: + case PixelFormat::Stencil8UI: + return 1; + case PixelFormat::R16Unorm: + case PixelFormat::RG16Unorm: + case PixelFormat::RGB16Unorm: + case PixelFormat::RGBA16Unorm: + case PixelFormat::R16Snorm: + case PixelFormat::RG16Snorm: + case PixelFormat::RGB16Snorm: + case PixelFormat::RGBA16Snorm: + case PixelFormat::R16UI: + case PixelFormat::RG16UI: + case PixelFormat::RGB16UI: + case PixelFormat::RGBA16UI: + case PixelFormat::R16I: + case PixelFormat::RG16I: + case PixelFormat::RGB16I: + case PixelFormat::RGBA16I: + case PixelFormat::R16F: + case PixelFormat::RG16F: + case PixelFormat::RGB16F: + case PixelFormat::RGBA16F: + case PixelFormat::Depth16Unorm: + case PixelFormat::Depth16UnormStencil8UI: + return 2; + case PixelFormat::R32UI: + case PixelFormat::RG32UI: + case PixelFormat::RGB32UI: + case PixelFormat::RGBA32UI: + case PixelFormat::R32I: + case PixelFormat::RG32I: + case PixelFormat::RGB32I: + case PixelFormat::RGBA32I: + case PixelFormat::R32F: + case PixelFormat::RG32F: + case PixelFormat::RGB32F: + case PixelFormat::RGBA32F: + case PixelFormat::Depth24Unorm: + case PixelFormat::Depth32F: + case PixelFormat::Depth24UnormStencil8UI: + case PixelFormat::Depth32FStencil8UI: + return 4; + } + + CORRADE_ASSERT_UNREACHABLE("componentSize(): unsupported format" << format, {}); /* LCOV_EXCL_LINE */ +} + +UnsignedByte formatTypeSize(CompressedPixelFormat) { + return 1; +} + +struct SampleData { + UnsignedShort bitOffset; + UnsignedShort bitLength; + Implementation::KdfBasicBlockSample::ChannelId id; + /* For pixel formats where not all channels share the same suffix (only + combined depth + stencil for now) we have to specify it manually */ + /** @todo Is there a good way to automate this in formatMapping.hpp? */ + Implementation::VkFormatSuffix suffix; +}; + +Containers::Pair> samples(PixelFormat format) { + constexpr auto ColorModel = Implementation::KdfBasicBlockHeader::ColorModel::Rgbsda; + + /* We later multiply the offset and length by the type size. This works as + long as the channels are all the same size. If PixelFormat ever supports + formats like R10G10B10A2 this needs to be changed. For depth formats + this assumption already doesn't hold, so we have to specialize and + later code needs to make sure to not multiply by the type size. */ + static constexpr SampleData SamplesRgba[]{ + {0, 8, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}}, + {8, 8, Implementation::KdfBasicBlockSample::ChannelId::Green, Implementation::VkFormatSuffix{}}, + {16, 8, Implementation::KdfBasicBlockSample::ChannelId::Blue, Implementation::VkFormatSuffix{}}, + {24, 8, Implementation::KdfBasicBlockSample::ChannelId::Alpha, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesDepth16Stencil[]{ + {0, 16, Implementation::KdfBasicBlockSample::ChannelId::Depth, Implementation::VkFormatSuffix::UNORM}, + {16, 8, Implementation::KdfBasicBlockSample::ChannelId::Stencil, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesDepth24Stencil[]{ + {0, 24, Implementation::KdfBasicBlockSample::ChannelId::Depth, Implementation::VkFormatSuffix::UNORM}, + {24, 8, Implementation::KdfBasicBlockSample::ChannelId::Stencil, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesDepth32FStencil[]{ + {0, 32, Implementation::KdfBasicBlockSample::ChannelId::Depth, Implementation::VkFormatSuffix::SFLOAT}, + {32, 8, Implementation::KdfBasicBlockSample::ChannelId::Stencil, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesStencil[]{ + {0, 8, Implementation::KdfBasicBlockSample::ChannelId::Stencil, Implementation::VkFormatSuffix{}} + }; + + switch(format) { + case PixelFormat::Stencil8UI: + return {ColorModel, SamplesStencil}; + case PixelFormat::Depth16Unorm: + return {ColorModel, Containers::arrayView(SamplesDepth16Stencil).prefix(1)}; + case PixelFormat::Depth16UnormStencil8UI: + return {ColorModel, SamplesDepth16Stencil}; + case PixelFormat::Depth24Unorm: + return {ColorModel, Containers::arrayView(SamplesDepth24Stencil).prefix(1)}; + case PixelFormat::Depth24UnormStencil8UI: + return {ColorModel, SamplesDepth24Stencil}; + case PixelFormat::Depth32F: + return {ColorModel, Containers::arrayView(SamplesDepth32FStencil).prefix(1)}; + case PixelFormat::Depth32FStencil8UI: + return {ColorModel, SamplesDepth32FStencil}; + default: { + const UnsignedInt size = pixelSize(format); + const UnsignedInt typeSize = formatTypeSize(format); + CORRADE_INTERNAL_ASSERT(size%typeSize == 0); + const UnsignedInt numChannels = size/typeSize; + return {ColorModel, Containers::arrayView(SamplesRgba).prefix(numChannels)}; + } + } +} + +Containers::Pair> samples(CompressedPixelFormat format) { + /* There is no good way to auto-generate these from data. The KDF spec has + a format.json (https://github.com/KhronosGroup/KTX-Specification/blob/master/formats.json) + but that doesn't contain any information on how to fill the DFD. + Then there's Khronos' own dfdutils (https://github.com/KhronosGroup/KTX-Software/tree/master/lib/dfdutils) + but that generates headers through Perl scripts, and the headers need + the original VkFormat enum to be defined. + + DFD content is taken directly from the KDF spec: + https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#CompressedFormatModels */ + + static constexpr SampleData SamplesBc1[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc1AlphaPunchThrough[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Bc1Alpha, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc2And3[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Alpha, Implementation::VkFormatSuffix{}}, + {64, 64, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc4[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc4Signed[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc5[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}}, + {64, 64, Implementation::KdfBasicBlockSample::ChannelId::Green, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc5Signed[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}}, + {64, 64, Implementation::KdfBasicBlockSample::ChannelId::Green, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc6h[]{ + {0, 128, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc6hSigned[]{ + {0, 128, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesBc7[]{ + {0, 128, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEacR11[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEacR11Signed[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEacRG11[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}}, + {64, 64, Implementation::KdfBasicBlockSample::ChannelId::Green, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEacRG11Signed[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Red, Implementation::VkFormatSuffix{}}, + {64, 64, Implementation::KdfBasicBlockSample::ChannelId::Green, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEtc2[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Etc2Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEtc2AlphaPunchThrough[]{ + /* Both samples have the same offset, the KDF spec wants it that way. + BC1 indicates punch-through alpha with a different channel id, + but ETC2 is special. */ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Etc2Color, Implementation::VkFormatSuffix{}}, + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Alpha, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesEtc2Alpha[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Alpha, Implementation::VkFormatSuffix{}}, + {64, 64, Implementation::KdfBasicBlockSample::ChannelId::Etc2Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesAstc[]{ + {0, 128, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesAstcHdr[]{ + {0, 128, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + static constexpr SampleData SamplesPvrtc[]{ + {0, 64, Implementation::KdfBasicBlockSample::ChannelId::Color, Implementation::VkFormatSuffix{}} + }; + + switch(format) { + case CompressedPixelFormat::Bc1RGBUnorm: + case CompressedPixelFormat::Bc1RGBSrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc1, SamplesBc1}; + case CompressedPixelFormat::Bc1RGBAUnorm: + case CompressedPixelFormat::Bc1RGBASrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc1, SamplesBc1AlphaPunchThrough}; + case CompressedPixelFormat::Bc2RGBAUnorm: + case CompressedPixelFormat::Bc2RGBASrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc2, SamplesBc2And3}; + case CompressedPixelFormat::Bc3RGBAUnorm: + case CompressedPixelFormat::Bc3RGBASrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc3, SamplesBc2And3}; + case CompressedPixelFormat::Bc4RUnorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc4, SamplesBc4}; + case CompressedPixelFormat::Bc4RSnorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc4, SamplesBc4Signed}; + case CompressedPixelFormat::Bc5RGUnorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc5, SamplesBc5}; + case CompressedPixelFormat::Bc5RGSnorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc5, SamplesBc5Signed}; + case CompressedPixelFormat::Bc6hRGBUfloat: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc6h, SamplesBc6h}; + case CompressedPixelFormat::Bc6hRGBSfloat: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc6h, SamplesBc6hSigned}; + case CompressedPixelFormat::Bc7RGBAUnorm: + case CompressedPixelFormat::Bc7RGBASrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Bc7, SamplesBc7}; + case CompressedPixelFormat::EacR11Unorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEacR11}; + case CompressedPixelFormat::EacR11Snorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEacR11Signed}; + case CompressedPixelFormat::EacRG11Unorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEacRG11}; + case CompressedPixelFormat::EacRG11Snorm: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEacRG11Signed}; + case CompressedPixelFormat::Etc2RGB8Unorm: + case CompressedPixelFormat::Etc2RGB8Srgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEtc2}; + case CompressedPixelFormat::Etc2RGB8A1Unorm: + case CompressedPixelFormat::Etc2RGB8A1Srgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEtc2AlphaPunchThrough}; + case CompressedPixelFormat::Etc2RGBA8Unorm: + case CompressedPixelFormat::Etc2RGBA8Srgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Etc2, SamplesEtc2Alpha}; + case CompressedPixelFormat::Astc4x4RGBAUnorm: + case CompressedPixelFormat::Astc4x4RGBASrgb: + case CompressedPixelFormat::Astc5x4RGBAUnorm: + case CompressedPixelFormat::Astc5x4RGBASrgb: + case CompressedPixelFormat::Astc5x5RGBAUnorm: + case CompressedPixelFormat::Astc5x5RGBASrgb: + case CompressedPixelFormat::Astc6x5RGBAUnorm: + case CompressedPixelFormat::Astc6x5RGBASrgb: + case CompressedPixelFormat::Astc6x6RGBAUnorm: + case CompressedPixelFormat::Astc6x6RGBASrgb: + case CompressedPixelFormat::Astc8x5RGBAUnorm: + case CompressedPixelFormat::Astc8x5RGBASrgb: + case CompressedPixelFormat::Astc8x6RGBAUnorm: + case CompressedPixelFormat::Astc8x6RGBASrgb: + case CompressedPixelFormat::Astc8x8RGBAUnorm: + case CompressedPixelFormat::Astc8x8RGBASrgb: + case CompressedPixelFormat::Astc10x5RGBAUnorm: + case CompressedPixelFormat::Astc10x5RGBASrgb: + case CompressedPixelFormat::Astc10x6RGBAUnorm: + case CompressedPixelFormat::Astc10x6RGBASrgb: + case CompressedPixelFormat::Astc10x8RGBAUnorm: + case CompressedPixelFormat::Astc10x8RGBASrgb: + case CompressedPixelFormat::Astc10x10RGBAUnorm: + case CompressedPixelFormat::Astc10x10RGBASrgb: + case CompressedPixelFormat::Astc12x10RGBAUnorm: + case CompressedPixelFormat::Astc12x10RGBASrgb: + case CompressedPixelFormat::Astc12x12RGBAUnorm: + case CompressedPixelFormat::Astc12x12RGBASrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Astc, SamplesAstc}; + case CompressedPixelFormat::Astc4x4RGBAF: + case CompressedPixelFormat::Astc5x4RGBAF: + case CompressedPixelFormat::Astc5x5RGBAF: + case CompressedPixelFormat::Astc6x5RGBAF: + case CompressedPixelFormat::Astc6x6RGBAF: + case CompressedPixelFormat::Astc8x5RGBAF: + case CompressedPixelFormat::Astc8x6RGBAF: + case CompressedPixelFormat::Astc8x8RGBAF: + case CompressedPixelFormat::Astc10x5RGBAF: + case CompressedPixelFormat::Astc10x6RGBAF: + case CompressedPixelFormat::Astc10x8RGBAF: + case CompressedPixelFormat::Astc10x10RGBAF: + case CompressedPixelFormat::Astc12x10RGBAF: + case CompressedPixelFormat::Astc12x12RGBAF: + return {Implementation::KdfBasicBlockHeader::ColorModel::Astc, SamplesAstcHdr}; + /* 3D ASTC formats are not exposed in Vulkan */ + case CompressedPixelFormat::PvrtcRGB2bppUnorm: + case CompressedPixelFormat::PvrtcRGB2bppSrgb: + case CompressedPixelFormat::PvrtcRGBA2bppUnorm: + case CompressedPixelFormat::PvrtcRGBA2bppSrgb: + case CompressedPixelFormat::PvrtcRGB4bppUnorm: + case CompressedPixelFormat::PvrtcRGB4bppSrgb: + case CompressedPixelFormat::PvrtcRGBA4bppUnorm: + case CompressedPixelFormat::PvrtcRGBA4bppSrgb: + return {Implementation::KdfBasicBlockHeader::ColorModel::Pvrtc, SamplesPvrtc}; + default: + /* Default switch to suppress warnings about unhandled 3D ASTC formats */ + break; /* LCOV_EXCL_LINE */ + } + + CORRADE_ASSERT_UNREACHABLE("samples(): unsupported format" << format, {}); /* LCOV_EXCL_LINE */ +} + +UnsignedByte channelFormat(Implementation::VkFormatSuffix suffix) { + switch(suffix) { + case Implementation::VkFormatSuffix::UNORM: + return {}; + case Implementation::VkFormatSuffix::SNORM: + return Implementation::KdfBasicBlockSample::ChannelFormat::Signed; + case Implementation::VkFormatSuffix::UINT: + return {}; + case Implementation::VkFormatSuffix::SINT: + return Implementation::KdfBasicBlockSample::ChannelFormat::Signed; + case Implementation::VkFormatSuffix::UFLOAT: + return Implementation::KdfBasicBlockSample::ChannelFormat::Float; + case Implementation::VkFormatSuffix::SFLOAT: + return Implementation::KdfBasicBlockSample::ChannelFormat::Float | + Implementation::KdfBasicBlockSample::ChannelFormat::Signed; + case Implementation::VkFormatSuffix::SRGB: + return {}; + } + + CORRADE_ASSERT_UNREACHABLE("channelFormat(): invalid format suffix" << UnsignedInt(suffix), {}); /* LCOV_EXCL_LINE */ +} + +Containers::Pair channelMapping(Implementation::VkFormatSuffix suffix, UnsignedInt bitLength, bool isCompressed) { + /* sampleLower and sampleUpper define how to interpret the range of values + found in a channel. + samplerLower = black value or -1 for signed values + samplerUpper = white value or 1 for signed values + + There are a lot more weird subtleties for other color modes but this + simple version is enough for our needs. + + Signed integer values are sign-extended. Floats need to be bitcast. */ + CORRADE_INTERNAL_ASSERT(bitLength <= 32); + + const UnsignedInt typeMask = ~0u >> (32 - bitLength); + + switch(suffix) { + case Implementation::VkFormatSuffix::UNORM: + case Implementation::VkFormatSuffix::SRGB: + return {0u, typeMask}; + case Implementation::VkFormatSuffix::SNORM: { + /* Remove sign bit to get largest positive value. If we flip the + bits of that, we get the sign-extended smallest negative value. */ + const UnsignedInt positiveTypeMask = typeMask >> 1; + /* Uncompressed formats need -MAX (= MIN + 1) for symmetry around 0 + but block-compressed formats need INT32_MIN according to the + KDF spec. */ + return {~positiveTypeMask + UnsignedInt(!isCompressed), positiveTypeMask}; + } + case Implementation::VkFormatSuffix::UINT: + return {0u, 1u}; + case Implementation::VkFormatSuffix::SINT: + return {~0u, 1u}; + case Implementation::VkFormatSuffix::UFLOAT: + return {Corrade::Utility::bitCast(0.0f), Corrade::Utility::bitCast(1.0f)}; + case Implementation::VkFormatSuffix::SFLOAT: + return {Corrade::Utility::bitCast(-1.0f), Corrade::Utility::bitCast(1.0f)}; + } + + CORRADE_ASSERT_UNREACHABLE("channelMapping(): invalid format suffix" << UnsignedInt(suffix), {}); /* LCOV_EXCL_LINE */ +} + +template +Containers::Array fillDataFormatDescriptor(Format format, Implementation::VkFormatSuffix suffix) { + const auto sampleData = samples(format); + CORRADE_INTERNAL_ASSERT(!sampleData.second().empty()); + + /* Calculate total size. Header + one sample block per channel. */ + const std::size_t dfdSamplesSize = sampleData.second().size()*sizeof(Implementation::KdfBasicBlockSample); + const std::size_t dfdBlockSize = sizeof(Implementation::KdfBasicBlockHeader) + dfdSamplesSize; + const std::size_t dfdSize = sizeof(UnsignedInt) + dfdBlockSize; + CORRADE_INTERNAL_ASSERT(dfdSize%4 == 0); + + Containers::Array data{ValueInit, dfdSize}; + std::size_t offset = 0; + + UnsignedInt& length = *reinterpret_cast(data.suffix(offset).data()); + offset += sizeof(length); + + length = dfdSize; + + /* Basic block header */ + Implementation::KdfBasicBlockHeader& header = *reinterpret_cast(data.suffix(offset).data()); + offset += sizeof(header); + + header.vendorId = Implementation::KdfBasicBlockHeader::VendorId::Khronos; + header.descriptorType = Implementation::KdfBasicBlockHeader::DescriptorType::Basic; + header.versionNumber = Implementation::KdfBasicBlockHeader::VersionNumber::Kdf1_3; + header.descriptorBlockSize = dfdBlockSize; + + header.colorModel = sampleData.first(); + header.colorPrimaries = Implementation::KdfBasicBlockHeader::ColorPrimaries::Srgb; + header.transferFunction = suffix == Implementation::VkFormatSuffix::SRGB + ? Implementation::KdfBasicBlockHeader::TransferFunction::Srgb + : Implementation::KdfBasicBlockHeader::TransferFunction::Linear; + /** @todo Do we ever have premultiplied alpha? */ + + const Vector3i unitSize = formatUnitSize(format); + const UnsignedInt unitDataSize = formatUnitDataSize(format); + + /* Value of texelBlockDimension is saved as one less than the actual size. + The intent is to allow 256 but it's a wonderful bug source. */ + for(UnsignedInt i = 0; i != unitSize.Size; ++i) { + if(unitSize[i] > 1) + header.texelBlockDimension[i] = unitSize[i] - 1; + } + + /* Sample blocks, one per channel */ + auto samples = Containers::arrayCast(data.suffix(offset)); + offset += dfdSamplesSize; + + constexpr bool isCompressedFormat = std::is_same::value; + const bool isDepthStencil = !isCompressedFormat && + sampleData.second().front().id != Implementation::KdfBasicBlockSample::ChannelId::Red; + + const UnsignedByte typeSize = formatTypeSize(format); + /* Compressed integer formats must use 32-bit lower/upper */ + const UnsignedByte mappingBitLength = (isCompressedFormat ? sizeof(UnsignedInt) : typeSize)*8; + /* @todo BC6h has unsigned floats, but the spec says to use a sampleLower + of -1.0. The signed channel format flag is still set, however. + See https://github.com/KhronosGroup/DataFormat/issues/16 */ + const auto lowerUpper = channelMapping(suffix, mappingBitLength, isCompressedFormat); + const UnsignedByte formatFlags = channelFormat(suffix); + /* For non-compressed RGBA channels, we get the 1-byte channel data + and then multiply by the actual typeSize in the loop below */ + const UnsignedByte bitRangeMultiplier = isDepthStencil ? 1 : typeSize; + + UnsignedShort extent = 0; + for(UnsignedInt i = 0; i != samples.size(); ++i) { + const auto& sampleContent = sampleData.second()[i]; + auto& sample = samples[i]; + /* Value of bitLength is saved as one less than the actual size */ + sample.bitOffset = sampleContent.bitOffset*bitRangeMultiplier; + sample.bitLength = sampleContent.bitLength*bitRangeMultiplier - 1; + + /* Some channels have custom suffixes, can't use data calculated + from the main suffix */ + UnsignedByte sampleFormatFlags; + Containers::Pair sampleLowerUpper; + if(sampleContent.suffix != Implementation::VkFormatSuffix{}) { + CORRADE_INTERNAL_ASSERT(!isCompressedFormat); + sampleFormatFlags = channelFormat(sampleContent.suffix); + sampleLowerUpper = channelMapping(sampleContent.suffix, sample.bitLength + 1, isCompressedFormat); + } else { + sampleFormatFlags = formatFlags; + sampleLowerUpper = lowerUpper; + } + + sample.channelType = sampleContent.id | sampleFormatFlags; + sample.lower = sampleLowerUpper.first(); + sample.upper = sampleLowerUpper.second(); + + /* The linear format flag should only be set when the transfer function + is non-linear */ + if(header.transferFunction != Implementation::KdfBasicBlockHeader::TransferFunction::Linear && + sampleContent.id == Implementation::KdfBasicBlockSample::ChannelId::Alpha) + { + sample.channelType |= Implementation::KdfBasicBlockSample::ChannelFormat::Linear; + } + + extent = Math::max(sample.bitOffset + sample.bitLength + 1, extent); + + Utility::Endianness::littleEndianInPlace(sample.bitOffset, + sample.lower, sample.upper); + } + + /* Make sure channel bit ranges returned by samples() are plausible. + Can't use equals because some formats have channels smaller than the + pixel size (mainly the combined depth formats). */ + CORRADE_INTERNAL_ASSERT(extent%8 == 0); + CORRADE_INTERNAL_ASSERT(extent <= unitDataSize*8); + + /* The byte count is the actual occupied number of bytes. For most formats + this is equal to unitDataSize, but for some formats with different-sized + channels it can be less (e.g. Depth16UnormStencil8UI). Depth24Unorm is + an odd exception because as far as Vulkan is concerned, it's a packed + type (_PACK32), so the byte count is 4, not 3. The check below works + because Depth24Unorm is the only single-channel format where + extent/8 < unitDataSize. */ + if(samples.size() > 1) + header.bytesPlane[0] = extent/8; + else + header.bytesPlane[0] = unitDataSize; + + CORRADE_INTERNAL_ASSERT(offset == dfdSize); + + Utility::Endianness::littleEndianInPlace(length); + Utility::Endianness::littleEndianInPlace(header.vendorId, header.descriptorType, + header.versionNumber, header.descriptorBlockSize); + + return data; +} + +UnsignedInt leastCommonMultiple(UnsignedInt a, UnsignedInt b) { + const UnsignedInt product = a*b; + + /* Greatest common divisor */ + while(b != 0) { + const UnsignedInt t = a%b; + a = b; + b = t; + } + const UnsignedInt gcd = a; + + return product/gcd; +} + +template +void copyPixels(const BasicImageView& image, Containers::ArrayView pixels) { + /* Copy the pixels into output, dropping padding (if any) */ + const Containers::StridedArrayView srcPixels = image.pixels(); + Utility::copy(srcPixels, Containers::StridedArrayView{pixels, srcPixels.size()}); +} + +template +void copyPixels(const BasicCompressedImageView& image, Containers::ArrayView pixels) { + /** @todo Support CompressedPixelStorage::skip */ + CORRADE_ASSERT(image.storage() == CompressedPixelStorage{}, "Trade::KtxImageConverter::convertToData(): non-default compressed storage is not supported", ); + Utility::copy(image.data().prefix(pixels.size()), pixels); +} + +template struct TypeForSize {}; +template<> struct TypeForSize<1> { typedef UnsignedByte Type; }; +template<> struct TypeForSize<2> { typedef UnsignedShort Type; }; +template<> struct TypeForSize<4> { typedef UnsignedInt Type; }; +template<> struct TypeForSize<8> { typedef UnsignedLong Type; }; + +void endianSwap(Containers::ArrayView data, UnsignedInt typeSize) { + switch(typeSize) { + case 1: + /* Single-byte or block-compressed format, nothing to do */ + return; + case 2: + Utility::Endianness::littleEndianInPlace(Containers::arrayCast::Type>(data)); + return; + case 4: + Utility::Endianness::littleEndianInPlace(Containers::arrayCast::Type>(data)); + return; + case 8: + Utility::Endianness::littleEndianInPlace(Containers::arrayCast::Type>(data)); + return; + } + + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +using namespace Containers::Literals; + +/* Having this inside convertLevels() leads to errors with GCC 4.8: + "cannot initialize aggregate [...] with a compound literal" */ +constexpr Containers::StringView ValidOrientations[3]{"rl"_s, "du"_s, "io"_s}; + +/* Using a template template parameter to deduce the image dimensions while + matching both ImageView and CompressedImageView. Matching on the ImageView + typedefs doesn't work, so we need the extra parameter of BasicImageView. */ +template class View> +Containers::Array convertLevels(Containers::ArrayView> imageLevels, const Corrade::Utility::ConfigurationGroup& configuration) { + const auto format = imageLevels.front().format(); + if(isFormatImplementationSpecific(format)) { + Error{} << "Trade::KtxImageConverter::convertToData(): implementation-specific formats are not supported"; + return {}; + } + + const auto vkFormat = vulkanFormat(format); + if(vkFormat.first() == Implementation::VK_FORMAT_UNDEFINED) { + Error{} << "Trade::KtxImageConverter::convertToData(): unsupported format" << format; + return {}; + } + + const Containers::Array dataFormatDescriptor = fillDataFormatDescriptor(format, vkFormat.second()); + + /* Fill key/value data. Values can be any byte-string but we only write + constant text strings. Keys must be sorted alphabetically. + Entries with an empty value won't be written. */ + + const std::string orientation = configuration.value("orientation"); + const std::string swizzle = configuration.value("swizzle"); + const std::string writerName = configuration.value("writerName"); + + if(!orientation.empty()) { + if(orientation.size() < dimensions) { + Error{} << "Trade::KtxImageConverter::convertToData(): invalid orientation string, expected at least" << + dimensions << "characters but got" << orientation; + return {}; + } + + for(UnsignedByte i = 0; i != dimensions; ++i) { + if(!ValidOrientations[i].contains(orientation[i])) { + /* Error{} prints char as int value so use StringViews to get + text output */ + Error{} << "Trade::KtxImageConverter::convertToData(): invalid character in orientation, expected" << + ValidOrientations[i].prefix(1) << "or" << ValidOrientations[i].suffix(1) << + "but got" << Containers::StringView{orientation}.suffix(i).prefix(1); + return {}; + } + } + } + + if(!swizzle.empty() && swizzle.size() != 4) { + Error{} << "Trade::KtxImageConverter::convertToData(): invalid swizzle length, expected 4 but got" << swizzle.size(); + return {}; + } + + if(swizzle.find_first_not_of("rgba01") != std::string::npos) { + Error{} << "Trade::KtxImageConverter::convertToData(): invalid characters in swizzle" << swizzle; + return {}; + } + + const Containers::Pair keyValueMap[]{ + Containers::pair("KTXorientation"_s, Containers::StringView{orientation}.prefix(Math::min(size_t(dimensions), orientation.size()))), + Containers::pair("KTXswizzle"_s, Containers::StringView{swizzle}), + Containers::pair("KTXwriter"_s, Containers::StringView{writerName}) + }; + + /* Calculate size */ + std::size_t kvdSize = 0; + for(const auto& entry: keyValueMap) { + CORRADE_INTERNAL_ASSERT(!entry.first().isEmpty()); + if(!entry.second().isEmpty()) { + const UnsignedInt length = entry.first().size() + 1 + entry.second().size() + 1; + kvdSize += sizeof(length) + (length + 3)/4*4; + } + } + CORRADE_INTERNAL_ASSERT(kvdSize%4 == 0); + + /* Pack. We assume that values are text strings, no endian-swapping needed. */ + std::size_t kvdOffset = 0; + Containers::Array keyValueData{ValueInit, kvdSize}; + for(const auto& entry: keyValueMap) { + if(!entry.second().isEmpty()) { + const auto key = entry.first(); + const auto value = entry.second(); + const UnsignedInt length = key.size() + 1 + value.size() + 1; + *reinterpret_cast(keyValueData.suffix(kvdOffset).data()) = length; + Utility::Endianness::littleEndianInPlace(length); + kvdOffset += sizeof(length); + Utility::copy(key, keyValueData.suffix(kvdOffset).prefix(key.size())); + kvdOffset += entry.first().size() + 1; + Utility::copy(value, keyValueData.suffix(kvdOffset).prefix(value.size())); + kvdOffset += entry.second().size() + 1; + kvdOffset = (kvdOffset + 3)/4*4; + } + } + CORRADE_INTERNAL_ASSERT(kvdOffset == kvdSize); + + /* Fill level index */ + const Math::Vector size = imageLevels.front().size(); + + const UnsignedInt numMipmaps = Math::min(imageLevels.size(), Math::log2(size.max()) + 1); + if(imageLevels.size() > numMipmaps) { + Error{} << "Trade::KtxImageConverter::convertToData(): there can be only" << numMipmaps << + "levels with base image size" << imageLevels[0].size() << "but got" << imageLevels.size(); + return {}; + } + + Containers::Array levelIndex{numMipmaps}; + + const std::size_t levelIndexSize = numMipmaps*sizeof(Implementation::KtxLevel); + std::size_t levelOffset = sizeof(Implementation::KtxHeader) + levelIndexSize + + dataFormatDescriptor.size() + keyValueData.size(); + + /* A "unit" is either a pixel or a block in a compressed format */ + const Vector3i unitSize = formatUnitSize(format); + const UnsignedInt unitDataSize = formatUnitDataSize(format); + + for(UnsignedInt i = 0; i != levelIndex.size(); ++i) { + /* Mip levels are required to be stored from smallest to largest for + efficient streaming */ + const UnsignedInt mip = levelIndex.size() - 1 - i; + const Math::Vector mipSize = Math::max(size >> mip, 1); + + const auto& image = imageLevels[mip]; + + if(image.size() != mipSize) { + Error() << "Trade::KtxImageConverter::convertToData(): expected " + "size" << mipSize << "for level" << mip << "but got" << image.size(); + return {}; + } + + /* Offset needs to be aligned to the least common multiple of the + texel/block size and 4. Not needed with supercompression. */ + const std::size_t alignment = leastCommonMultiple(unitDataSize, 4); + levelOffset = (levelOffset + alignment - 1)/alignment*alignment; + + const Vector3i unitCount = (Vector3i::pad(mipSize, 1) + unitSize - Vector3i{1})/unitSize; + const std::size_t levelSize = unitDataSize*unitCount.product(); + + levelIndex[mip].byteOffset = levelOffset; + levelIndex[mip].byteLength = levelSize; + levelIndex[mip].uncompressedByteLength = levelSize; + + levelOffset += levelSize; + } + + const std::size_t dataSize = levelOffset; + Containers::Array data{ValueInit, dataSize}; + + std::size_t offset = 0; + + /* Fill header */ + auto& header = *reinterpret_cast(data.data()); + offset += sizeof(header); + Utility::copy(Containers::arrayView(Implementation::KtxFileIdentifier), Containers::arrayView(header.identifier)); + + header.vkFormat = vkFormat.first(); + header.typeSize = formatTypeSize(format); + header.imageSize = Vector3ui{Vector3i::pad(size, 0u)}; + /** @todo Handle different image types (cube and/or array) once this can be + queried from images */ + header.layerCount = 0; + header.faceCount = 1; + header.levelCount = levelIndex.size(); + header.supercompressionScheme = Implementation::SuperCompressionScheme::None; + + for(UnsignedInt i = 0; i != levelIndex.size(); ++i) { + const Implementation::KtxLevel& level = levelIndex[i]; + const auto& image = imageLevels[i]; + const auto pixels = data.suffix(level.byteOffset).prefix(level.byteLength); + copyPixels(image, pixels); + + endianSwap(pixels, header.typeSize); + + Utility::Endianness::littleEndianInPlace( + level.byteOffset, level.byteLength, + level.uncompressedByteLength); + } + + Utility::copy(Containers::arrayCast(levelIndex), data.suffix(offset).prefix(levelIndexSize)); + offset += levelIndexSize; + + header.dfdByteOffset = offset; + header.dfdByteLength = dataFormatDescriptor.size(); + offset += header.dfdByteLength; + + Utility::copy(dataFormatDescriptor, data.suffix(header.dfdByteOffset).prefix(header.dfdByteLength)); + + if(!keyValueData.empty()) { + header.kvdByteOffset = offset; + header.kvdByteLength = keyValueData.size(); + + Utility::copy(keyValueData, data.suffix(header.kvdByteOffset).prefix(header.kvdByteLength)); + } + + /* Endian-swap once we're done using the header data */ + Utility::Endianness::littleEndianInPlace( + header.vkFormat, header.typeSize, + header.imageSize[0], header.imageSize[1], header.imageSize[2], + header.layerCount, header.faceCount, header.levelCount, + header.supercompressionScheme, + header.dfdByteOffset, header.dfdByteLength, + header.kvdByteOffset, header.kvdByteLength); + + return data; +} + +} + +KtxImageConverter::KtxImageConverter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImageConverter{manager, plugin} {} + +ImageConverterFeatures KtxImageConverter::doFeatures() const { + return ImageConverterFeature::ConvertLevels1DToData | + ImageConverterFeature::ConvertLevels2DToData | + ImageConverterFeature::ConvertLevels3DToData | + ImageConverterFeature::ConvertCompressedLevels1DToData | + ImageConverterFeature::ConvertCompressedLevels2DToData | + ImageConverterFeature::ConvertCompressedLevels3DToData; +} + +Containers::Array KtxImageConverter::doConvertToData(Containers::ArrayView imageLevels) { + return convertLevels(imageLevels, configuration()); +} + +Containers::Array KtxImageConverter::doConvertToData(Containers::ArrayView imageLevels) { + return convertLevels(imageLevels, configuration()); +} + +Containers::Array KtxImageConverter::doConvertToData(Containers::ArrayView imageLevels) { + return convertLevels(imageLevels, configuration()); +} + +Containers::Array KtxImageConverter::doConvertToData(Containers::ArrayView imageLevels) { + return convertLevels(imageLevels, configuration()); +} + +Containers::Array KtxImageConverter::doConvertToData(Containers::ArrayView imageLevels) { + return convertLevels(imageLevels, configuration()); +} + +Containers::Array KtxImageConverter::doConvertToData(Containers::ArrayView imageLevels) { + return convertLevels(imageLevels, configuration()); +} + +}} + +CORRADE_PLUGIN_REGISTER(KtxImageConverter, Magnum::Trade::KtxImageConverter, + "cz.mosra.magnum.Trade.AbstractImageConverter/0.3.1") diff --git a/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.h b/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.h new file mode 100644 index 000000000..b532b66e3 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/KtxImageConverter.h @@ -0,0 +1,153 @@ +#ifndef Magnum_Trade_KtxImageConverter_h +#define Magnum_Trade_KtxImageConverter_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::Trade::KtxImageConverter + * @m_since_latest_{plugins} + */ + +#include + +#include "MagnumPlugins/KtxImageConverter/configure.h" + +#ifndef DOXYGEN_GENERATING_OUTPUT +#ifndef MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC + #ifdef KtxImageConverter_EXPORTS + #define MAGNUM_KTXIMAGECONVERTER_EXPORT CORRADE_VISIBILITY_EXPORT + #else + #define MAGNUM_KTXIMAGECONVERTER_EXPORT CORRADE_VISIBILITY_IMPORT + #endif +#else + #define MAGNUM_KTXIMAGECONVERTER_EXPORT CORRADE_VISIBILITY_STATIC +#endif +#define MAGNUM_KTXIMAGECONVERTER_LOCAL CORRADE_VISIBILITY_LOCAL +#else +#define MAGNUM_KTXIMAGECONVERTER_EXPORT +#define MAGNUM_KTXIMAGECONVERTER_LOCAL +#endif + +namespace Magnum { namespace Trade { + +/** +@brief KTX2 image converter plugin +@m_since_latest_{plugins} + +Creates Khronos Texture 2.0 (`*.ktx2`) files from images. You can use +@ref KtxImporter to import images in this format. + +@section Trade-KtxImageConverter-usage Usage + +This plugin depends on the @ref Trade library and is built if +`WITH_KTXIMAGECONVERTER` is enabled when building Magnum Plugins. To use as +a dynamic plugin, load @cpp "KtxImageConverter" @ce via +@ref Corrade::PluginManager::Manager. + +Additionally, if you're using Magnum as a CMake subproject, bundle the +[magnum-plugins](https://github.com/mosra/magnum-plugins) and do the following: + +@code{.cmake} +set(WITH_KTXIMAGECONVERTER ON CACHE BOOL "" FORCE) +add_subdirectory(magnum-plugins EXCLUDE_FROM_ALL) + +# So the dynamically loaded plugin gets built implicitly +add_dependencies(your-app MagnumPlugins::KtxImageConverter) +@endcode + +To use as a static plugin or as a dependency of another plugin with CMake, put +[FindMagnumPlugins.cmake](https://github.com/mosra/magnum-plugins/blob/master/modules/FindMagnumPlugins.cmake) +into your `modules/` directory, request the `KtxImageConverter` component +of the `MagnumPlugins` package and link to the +`MagnumPlugins::KtxImageConverter` target: + +@code{.cmake} +find_package(MagnumPlugins REQUIRED KtxImageConverter) + +# ... +target_link_libraries(your-app PRIVATE MagnumPlugins::KtxImageConverter) +@endcode + +See @ref building-plugins, @ref cmake-plugins, @ref plugins and +@ref file-formats for more information. + +@section Trade-KtxImageConverter-behavior Behavior and limitations + +@subsection Trade-KtxImageConverter-behavior-formats Supported formats + +The following formats can be written: + +- all formats in @ref PixelFormat +- all formats in @ref CompressedPixelFormat, except for 3D ASTC formats + +@subsection Trade-KtxImageConverter-behavior-cube-array Cube map and array images + +Cube map and array images can be written but there is currently no way to mark +them properly in the metadata. Exported files will be 3D images with cube map +faces and array layers exposed as depth slices. + +@subsection Trade-KtxImageConverter-behavior-multilevel Multilevel images + +All image types 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. + +@subsection Trade-KtxImageConverter-behavior-supercompression Supercompression + +Saving files with [supercompression](https://github.khronos.org/KTX-Specification/#supercompressionSchemes) +is not supported. + +@section Trade-KtxImageConverter-configuration Plugin-specific configuration + +It's possible to tune various metadata options through @ref configuration(). +See below for all options and their default values: + +@snippet MagnumPlugins/KtxImageConverter/KtxImageConverter.conf config + +See @ref plugins-configuration for more information and an example showing how +to edit the configuration values. +*/ +class MAGNUM_KTXIMAGECONVERTER_EXPORT KtxImageConverter: public AbstractImageConverter { + public: + /** @brief Plugin manager constructor */ + explicit KtxImageConverter(PluginManager::AbstractManager& manager, const std::string& plugin); + + private: + ImageConverterFeatures MAGNUM_KTXIMAGECONVERTER_LOCAL doFeatures() const override; + + Containers::Array MAGNUM_KTXIMAGECONVERTER_LOCAL doConvertToData(Containers::ArrayView imageLevels) override; + Containers::Array MAGNUM_KTXIMAGECONVERTER_LOCAL doConvertToData(Containers::ArrayView imageLevels) override; + Containers::Array MAGNUM_KTXIMAGECONVERTER_LOCAL doConvertToData(Containers::ArrayView imageLevels) override; + + Containers::Array MAGNUM_KTXIMAGECONVERTER_LOCAL doConvertToData(Containers::ArrayView imageLevels) override; + Containers::Array MAGNUM_KTXIMAGECONVERTER_LOCAL doConvertToData(Containers::ArrayView imageLevels) override; + Containers::Array MAGNUM_KTXIMAGECONVERTER_LOCAL doConvertToData(Containers::ArrayView imageLevels) override; +}; + +}} + +#endif diff --git a/src/MagnumPlugins/KtxImageConverter/Test/CMakeLists.txt b/src/MagnumPlugins/KtxImageConverter/Test/CMakeLists.txt new file mode 100644 index 000000000..8b17313c8 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/Test/CMakeLists.txt @@ -0,0 +1,122 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# 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"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(KTXIMPORTER_TEST_DIR ".") + set(KTXIMAGECONVERTER_TEST_DIR ".") +else() + set(KTXIMPORTER_TEST_DIR ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test) + set(KTXIMAGECONVERTER_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + +# CMake before 3.8 has broken $ expressions for iOS (see +# https://gitlab.kitware.com/cmake/cmake/merge_requests/404) and since Corrade +# doesn't support dynamic plugins on iOS, this sorta works around that. Should +# be revisited when updating Travis to newer Xcode (xcode7.3 has CMake 3.6). +if(NOT MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC) + set(KTXIMAGECONVERTER_PLUGIN_FILENAME $) + if(WITH_KTXIMPORTER) + set(KTXIMPORTER_PLUGIN_FILENAME $) + endif() +endif() + +# First replace ${} variables, then $<> generator expressions +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h + INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) + +corrade_add_test(KtxImageConverterTest KtxImageConverterTest.cpp + LIBRARIES Magnum::Trade + FILES + dfd-data.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-mipmaps.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip0.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip1.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip2.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-rgb.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-rgb32.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-rgbf32.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-s8.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-d16.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-d24s8.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-d32fs8.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-incomplete.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip0.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip1.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip2.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip3.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-mipmaps.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps.ktx2 + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip0.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip1.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip2.bin + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip3.bin) + +target_include_directories(KtxImageConverterTest PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/$ + ${PROJECT_SOURCE_DIR}/src) +if(MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC) + target_link_libraries(KtxImageConverterTest PRIVATE KtxImageConverter) + if(WITH_KTXIMPORTER) + target_link_libraries(KtxImageConverterTest PRIVATE KtxImporter) + endif() +else() + # So the plugins get properly built when building the test + add_dependencies(KtxImageConverterTest KtxImageConverter) + if(WITH_KTXIMPORTER) + add_dependencies(KtxImageConverterTest KtxImporter) + endif() +endif() +set_target_properties(KtxImageConverterTest PROPERTIES FOLDER "MagnumPlugins/KtxImageConverter/Test") +if(CORRADE_BUILD_STATIC AND NOT MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC) + # CMake < 3.4 does this implicitly, but 3.4+ not anymore (see CMP0065). + # That's generally okay, *except if* the build is static, the executable + # uses a plugin manager and needs to share globals with the plugins (such + # as output redirection and so on). + set_target_properties(KtxImageConverterTest PROPERTIES ENABLE_EXPORTS ON) +endif() diff --git a/src/MagnumPlugins/KtxImageConverter/Test/KtxImageConverterTest.cpp b/src/MagnumPlugins/KtxImageConverter/Test/KtxImageConverterTest.cpp new file mode 100644 index 000000000..675598c8d --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/Test/KtxImageConverterTest.cpp @@ -0,0 +1,1133 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MagnumPlugins/KtxImporter/KtxHeader.h" + +#include "configure.h" + +namespace Magnum { namespace Trade { namespace Test { namespace { + +struct KtxImageConverterTest: TestSuite::Tester { + explicit KtxImageConverterTest(); + + void supportedFormat(); + void supportedCompressedFormat(); + void unsupportedCompressedFormat(); + void implementationSpecificFormat(); + void implementationSpecificCompressedFormat(); + + void dataFormatDescriptor(); + void dataFormatDescriptorCompressed(); + + /* Non-default compressed pixel storage is currently not supported. + It's firing an internal assert, so we're not testing that. */ + void pixelStorage(); + + void tooManyLevels(); + void levelWrongSize(); + + void convert1D(); + void convert1DMipmaps(); + void convert1DCompressed(); + void convert1DCompressedMipmaps(); + + void convert2D(); + void convert2DMipmaps(); + /* Should be enough to only test this for one type */ + void convert2DMipmapsIncomplete(); + void convert2DCompressed(); + void convert2DCompressedMipmaps(); + + void convert3D(); + void convert3DMipmaps(); + void convert3DCompressed(); + void convert3DCompressedMipmaps(); + + /** @todo Add tests for cube and layered (and combined) images once the + converter supports those */ + + void convertFormats(); + + void pvrtcRgb(); + + void configurationOrientation(); + void configurationOrientationLessDimensions(); + void configurationOrientationEmpty(); + void configurationOrientationInvalid(); + void configurationSwizzle(); + void configurationSwizzleEmpty(); + void configurationSwizzleInvalid(); + void configurationWriterName(); + void configurationWriterNameEmpty(); + + void configurationEmpty(); + void configurationSorted(); + + void convertTwice(); + + /* Explicitly forbid system-wide plugin dependencies */ + PluginManager::Manager _converterManager{"nonexistent"}; + PluginManager::Manager _importerManager{"nonexistent"}; + + Containers::Array dfdData; + std::unordered_map> dfdMap; +}; + +using namespace Containers::Literals; +using namespace Math::Literals; + +/* Origin top-left-back */ +const Color3ub PatternRgbData[3][3][4]{ + /* black.png */ + {{0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb}, + {0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb}, + {0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb}}, + /* pattern.png */ + {{0x0000ff_rgb, 0x00ff00_rgb, 0x7f007f_rgb, 0x7f007f_rgb}, + {0xffffff_rgb, 0xff0000_rgb, 0x000000_rgb, 0x00ff00_rgb}, + {0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x00ff00_rgb}}, + /* pattern.png */ + {{0x0000ff_rgb, 0x00ff00_rgb, 0x7f007f_rgb, 0x7f007f_rgb}, + {0xffffff_rgb, 0xff0000_rgb, 0x000000_rgb, 0x00ff00_rgb}, + {0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x00ff00_rgb}} +}; + +/* Output of PVRTexTool with format conversion. This is PatternRgbData[2], + but each byte extended to uint by just repeating the byte 4 times. */ +constexpr UnsignedInt HalfU = 0x7f7f7f7f; +constexpr UnsignedInt FullU = 0xffffffff; +constexpr Math::Color3 PatternRgb32UIData[4*3]{ + { 0, 0, FullU}, { 0, FullU, 0}, {HalfU, 0, HalfU}, {HalfU, 0, HalfU}, + {FullU, FullU, FullU}, {FullU, 0, 0}, { 0, 0, 0}, { 0, FullU, 0}, + {FullU, 0, 0}, {FullU, FullU, FullU}, { 0, 0, 0}, { 0, FullU, 0} +}; + +/* Output of PVRTexTool with format conversion. This is PatternRgbData[2], + but each byte mapped to the range 0.0 - 1.0. */ +constexpr Float HalfF = 127.0f / 255.0f; +constexpr Math::Color3 PatternRgb32FData[4*3]{ + {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {HalfF, 0.0f, HalfF}, {HalfF, 0.0f, HalfF}, + {1.0f, 1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f} +}; + +constexpr UnsignedByte PatternStencil8UIData[4*3]{ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12 +}; + +constexpr UnsignedShort PatternDepth16UnormData[4*3]{ + 0xff01, 0xff02, 0xff03, 0xff04, + 0xff05, 0xff06, 0xff07, 0xff08, + 0xff09, 0xff10, 0xff11, 0xff12 +}; + +constexpr UnsignedInt PatternDepth24UnormStencil8UIData[4*3]{ + 0xffffff01, 0xffffff02, 0xffffff03, 0xffffff04, + 0xffffff05, 0xffffff06, 0xffffff07, 0xffffff08, + 0xffffff09, 0xffffff10, 0xffffff11, 0xffffff12 +}; + +constexpr UnsignedLong HalfL = 0x7f7f7f7f7f7f7f7f; +constexpr UnsignedLong FullL = 0xffffffffffffffff; +constexpr UnsignedLong PatternDepth32FStencil8UIData[4*3]{ + 0, 0, 0, HalfL, + 0, FullL, FullL, HalfL, + 0, FullL, 0, FullL +}; + +const char* WriterToktx = "toktx v4.0.0~6 / libktx v4.0.0~5"; +const char* WriterPVRTexTool = "PVRTexLib v5.1.0"; + +const struct { + const char* name; + const char* file; + const CompressedPixelFormat format; + const Math::Vector<1, Int> size; +} Convert1DCompressedData[]{ + {"BC1", "1d-compressed-bc1.ktx2", CompressedPixelFormat::Bc1RGBASrgb, {4}}, + {"ETC2", "1d-compressed-etc2.ktx2", CompressedPixelFormat::Etc2RGB8Srgb, {7}} +}; + +const struct { + const char* name; + const char* file; + const CompressedPixelFormat format; + const Vector2i size; +} Convert2DCompressedData[]{ + {"PVRTC", "2d-compressed-pvrtc.ktx2", CompressedPixelFormat::PvrtcRGBA4bppSrgb, {8, 8}}, + {"BC1", "2d-compressed-bc1.ktx2", CompressedPixelFormat::Bc1RGBASrgb, {8, 8}}, + {"BC3", "2d-compressed-bc3.ktx2", CompressedPixelFormat::Bc3RGBASrgb, {8, 8}}, + {"ETC2", "2d-compressed-etc2.ktx2", CompressedPixelFormat::Etc2RGB8Srgb, {9, 10}}, + {"ASTC", "2d-compressed-astc.ktx2", CompressedPixelFormat::Astc12x10RGBASrgb, {9, 10}} +}; + +const struct { + const char* name; + const char* file; + const char* orientation; + const char* writer; + const PixelFormat format; + const Containers::ArrayView data; + bool save; +} ConvertFormatsData[]{ + {"RGB32UI", "2d-rgb32.ktx2", "rd", WriterPVRTexTool, PixelFormat::RGB32UI, + Containers::arrayCast(PatternRgb32UIData), false}, + {"RGB32F", "2d-rgbf32.ktx2", "rd", WriterPVRTexTool, PixelFormat::RGB32F, + Containers::arrayCast(PatternRgb32FData), false}, + /* These are saved as test files for KtxImporterTest */ + {"Stencil8UI", "2d-s8.ktx2", nullptr, nullptr, PixelFormat::Stencil8UI, + Containers::arrayCast(PatternStencil8UIData), true}, + {"Depth16Unorm", "2d-d16.ktx2", nullptr, nullptr, PixelFormat::Depth16Unorm, + Containers::arrayCast(PatternDepth16UnormData), true}, + {"Depth24UnormStencil8UI", "2d-d24s8.ktx2", nullptr, nullptr, PixelFormat::Depth24UnormStencil8UI, + Containers::arrayCast(PatternDepth24UnormStencil8UIData), true}, + {"Depth32FStencil8UI", "2d-d32fs8.ktx2", nullptr, nullptr, PixelFormat::Depth32FStencil8UI, + Containers::arrayCast(PatternDepth32FStencil8UIData), true} +}; + +const struct { + const char* name; + const CompressedPixelFormat inputFormat; + const CompressedPixelFormat outputFormat; +} PvrtcRgbData[]{ + {"2bppUnorm", CompressedPixelFormat::PvrtcRGB2bppUnorm, CompressedPixelFormat::PvrtcRGBA2bppUnorm}, + {"2bppSrgb", CompressedPixelFormat::PvrtcRGB2bppSrgb, CompressedPixelFormat::PvrtcRGBA2bppSrgb}, + {"4bppUnorm", CompressedPixelFormat::PvrtcRGB4bppUnorm, CompressedPixelFormat::PvrtcRGBA4bppUnorm}, + {"4bppSrgb", CompressedPixelFormat::PvrtcRGB4bppSrgb, CompressedPixelFormat::PvrtcRGBA4bppSrgb}, +}; + +const struct { + const char* name; + const char* value; + const char* message; +} InvalidOrientationData[]{ + {"too short", "r", "invalid orientation string, expected at least 3 characters but got r"}, + {"invalid character", "xxx", "invalid character in orientation, expected r or l but got x"}, + {"invalid order", "rid", "invalid character in orientation, expected d or u but got i"}, +}; + +const struct { + const char* name; + const char* value; + const char* message; +} InvalidSwizzleData[]{ + {"too short", "r", "invalid swizzle length, expected 4 but got 1"}, + {"invalid characters", "rxba", "invalid characters in swizzle rxba"}, + {"invalid characters", "1012", "invalid characters in swizzle 1012"} +}; + +Containers::Array readDataFormatDescriptor(Containers::ArrayView fileData) { + CORRADE_INTERNAL_ASSERT(fileData.size() >= sizeof(Implementation::KtxHeader)); + const Implementation::KtxHeader& header = *reinterpret_cast(fileData.data()); + + const UnsignedInt offset = Utility::Endianness::littleEndian(header.dfdByteOffset); + const UnsignedInt length = Utility::Endianness::littleEndian(header.dfdByteLength); + Containers::Array data{ValueInit, length}; + Utility::copy(fileData.suffix(offset).prefix(length), data); + + return data; +} + +Containers::String readKeyValueData(Containers::ArrayView fileData) { + CORRADE_INTERNAL_ASSERT(fileData.size() >= sizeof(Implementation::KtxHeader)); + const Implementation::KtxHeader& header = *reinterpret_cast(fileData.data()); + + const UnsignedInt offset = Utility::Endianness::littleEndian(header.kvdByteOffset); + const UnsignedInt length = Utility::Endianness::littleEndian(header.kvdByteLength); + Containers::String data{ValueInit, length}; + Utility::copy(fileData.suffix(offset).prefix(length), data); + + return data; +} + +KtxImageConverterTest::KtxImageConverterTest() { + addTests({&KtxImageConverterTest::supportedFormat, + &KtxImageConverterTest::supportedCompressedFormat, + &KtxImageConverterTest::unsupportedCompressedFormat, + &KtxImageConverterTest::implementationSpecificFormat, + &KtxImageConverterTest::implementationSpecificCompressedFormat, + + &KtxImageConverterTest::dataFormatDescriptor, + &KtxImageConverterTest::dataFormatDescriptorCompressed, + + &KtxImageConverterTest::pixelStorage, + + &KtxImageConverterTest::tooManyLevels, + &KtxImageConverterTest::levelWrongSize, + + &KtxImageConverterTest::convert1D, + &KtxImageConverterTest::convert1DMipmaps}); + + addInstancedTests({&KtxImageConverterTest::convert1DCompressed}, + Containers::arraySize(Convert1DCompressedData)); + + addTests({&KtxImageConverterTest::convert1DCompressedMipmaps, + &KtxImageConverterTest::convert2D, + &KtxImageConverterTest::convert2DMipmaps, + &KtxImageConverterTest::convert2DMipmapsIncomplete}); + + addInstancedTests({&KtxImageConverterTest::convert2DCompressed}, + Containers::arraySize(Convert2DCompressedData)); + + addTests({&KtxImageConverterTest::convert2DCompressedMipmaps, + &KtxImageConverterTest::convert3D, + &KtxImageConverterTest::convert3DMipmaps, + &KtxImageConverterTest::convert3DCompressed, + &KtxImageConverterTest::convert3DCompressedMipmaps}); + + addInstancedTests({&KtxImageConverterTest::convertFormats}, + Containers::arraySize(ConvertFormatsData)); + + addInstancedTests({&KtxImageConverterTest::pvrtcRgb}, + Containers::arraySize(PvrtcRgbData)); + + addTests({&KtxImageConverterTest::configurationOrientation, + &KtxImageConverterTest::configurationOrientationLessDimensions, + &KtxImageConverterTest::configurationOrientationEmpty}); + + addInstancedTests({&KtxImageConverterTest::configurationOrientationInvalid}, + Containers::arraySize(InvalidOrientationData)); + + addTests({&KtxImageConverterTest::configurationSwizzle, + &KtxImageConverterTest::configurationSwizzleEmpty}); + + addInstancedTests({&KtxImageConverterTest::configurationSwizzleInvalid}, + Containers::arraySize(InvalidSwizzleData)); + + addTests({&KtxImageConverterTest::configurationWriterName, + &KtxImageConverterTest::configurationWriterNameEmpty, + &KtxImageConverterTest::configurationEmpty, + &KtxImageConverterTest::configurationSorted, + + &KtxImageConverterTest::convertTwice}); + + /* Load the plugin directly from the build tree. Otherwise it's static and + already loaded. */ + #ifdef KTXIMAGECONVERTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_converterManager.load(KTXIMAGECONVERTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + /* Optional plugins that don't have to be here */ + #ifdef KTXIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_importerManager.load(KTXIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + + /* Extract VkFormat and DFD content from merged DFD file */ + dfdData = Utility::Directory::read(Utility::Directory::join(KTXIMAGECONVERTER_TEST_DIR, "dfd-data.bin")); + CORRADE_INTERNAL_ASSERT(!dfdData.empty()); + CORRADE_INTERNAL_ASSERT(dfdData.size()%4 == 0); + std::size_t offset = 0; + while(offset < dfdData.size()) { + /* Each entry is a VkFormat, followed directly by the DFD. The first + uint32_t of the DFD is its size. */ + const Implementation::VkFormat format = *reinterpret_cast(dfdData.data() + offset); + offset += sizeof(format); + const UnsignedInt size = *reinterpret_cast(dfdData.data() + offset); + CORRADE_INTERNAL_ASSERT(size > 0); + CORRADE_INTERNAL_ASSERT(size%4 == 0); + dfdMap.emplace(format, dfdData.suffix(offset).prefix(size)); + offset += size; + } + CORRADE_INTERNAL_ASSERT(offset == dfdData.size()); +} + +void KtxImageConverterTest::supportedFormat() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[32]{}; + + /* All the formats in PixelFormat are supported */ + /** @todo This needs to be extended when new formats are added to + PixelFormat. In dataFormatDescriptor as well. */ + constexpr PixelFormat start = PixelFormat::R8Unorm; + constexpr PixelFormat end = PixelFormat::Depth32FStencil8UI; + + for(UnsignedInt format = UnsignedInt(start); format <= UnsignedInt(end); ++format) { + CORRADE_ITERATION(format); + CORRADE_INTERNAL_ASSERT(Containers::arraySize(bytes) >= pixelSize(PixelFormat(format))); + CORRADE_VERIFY(converter->convertToData(ImageView2D{PixelFormat(format), {1, 1}, bytes})); + } +} + +const CompressedPixelFormat UnsupportedCompressedFormats[]{ + /* Vulkan has no support (core or extension) for 3D ASTC formats. + KTX supports them, but through an unreleased extension. */ + CompressedPixelFormat::Astc3x3x3RGBAUnorm, + CompressedPixelFormat::Astc3x3x3RGBASrgb, + CompressedPixelFormat::Astc3x3x3RGBAF, + CompressedPixelFormat::Astc4x3x3RGBAUnorm, + CompressedPixelFormat::Astc4x3x3RGBASrgb, + CompressedPixelFormat::Astc4x3x3RGBAF, + CompressedPixelFormat::Astc4x4x3RGBAUnorm, + CompressedPixelFormat::Astc4x4x3RGBASrgb, + CompressedPixelFormat::Astc4x4x3RGBAF, + CompressedPixelFormat::Astc4x4x4RGBAUnorm, + CompressedPixelFormat::Astc4x4x4RGBASrgb, + CompressedPixelFormat::Astc4x4x4RGBAF, + CompressedPixelFormat::Astc5x4x4RGBAUnorm, + CompressedPixelFormat::Astc5x4x4RGBASrgb, + CompressedPixelFormat::Astc5x4x4RGBAF, + CompressedPixelFormat::Astc5x5x4RGBAUnorm, + CompressedPixelFormat::Astc5x5x4RGBASrgb, + CompressedPixelFormat::Astc5x5x4RGBAF, + CompressedPixelFormat::Astc5x5x5RGBAUnorm, + CompressedPixelFormat::Astc5x5x5RGBASrgb, + CompressedPixelFormat::Astc5x5x5RGBAF, + CompressedPixelFormat::Astc6x5x5RGBAUnorm, + CompressedPixelFormat::Astc6x5x5RGBASrgb, + CompressedPixelFormat::Astc6x5x5RGBAF, + CompressedPixelFormat::Astc6x6x5RGBAUnorm, + CompressedPixelFormat::Astc6x6x5RGBASrgb, + CompressedPixelFormat::Astc6x6x5RGBAF, + CompressedPixelFormat::Astc6x6x6RGBAUnorm, + CompressedPixelFormat::Astc6x6x6RGBASrgb, + CompressedPixelFormat::Astc6x6x6RGBAF +}; + +void KtxImageConverterTest::supportedCompressedFormat() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[32]{}; + + /** @todo This needs to be extended when new formats are added to + CompressedPixelFormat. In dataFormatDescriptorCompressed as well. */ + constexpr CompressedPixelFormat start = CompressedPixelFormat::Bc1RGBUnorm; + constexpr CompressedPixelFormat end = CompressedPixelFormat::PvrtcRGBA4bppSrgb; + + for(UnsignedInt format = UnsignedInt(start); format <= UnsignedInt(end); ++format) { + if(std::find(std::begin(UnsupportedCompressedFormats), std::end(UnsupportedCompressedFormats), + CompressedPixelFormat(format)) == std::end(UnsupportedCompressedFormats)) + { + CORRADE_ITERATION(format); + CORRADE_INTERNAL_ASSERT(Containers::arraySize(bytes) >= compressedBlockDataSize(CompressedPixelFormat(format))); + CORRADE_VERIFY(converter->convertToData(CompressedImageView2D{CompressedPixelFormat(format), {1, 1}, bytes})); + } + } +} + +void KtxImageConverterTest::unsupportedCompressedFormat() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[32]{}; + + for(CompressedPixelFormat format: UnsupportedCompressedFormats) { + CORRADE_ITERATION(format); + CORRADE_INTERNAL_ASSERT(Containers::arraySize(bytes) >= compressedBlockDataSize(CompressedPixelFormat(format))); + + CORRADE_VERIFY(!converter->convertToData(CompressedImageView2D{format, {1, 1}, bytes})); + + /* Not testing the output message so that it shows up as a friendly + nagging reminder to add support for these formats */ + } +} + +void KtxImageConverterTest::implementationSpecificFormat() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[1]{}; + + std::ostringstream out; + Error redirectError{&out}; + + PixelStorage storage; + storage.setAlignment(1); + CORRADE_VERIFY(!converter->convertToData(ImageView2D{storage, 0, 0, 1, {1, 1}, bytes})); + CORRADE_COMPARE(out.str(), + "Trade::KtxImageConverter::convertToData(): implementation-specific formats are not supported\n"); +} + +void KtxImageConverterTest::implementationSpecificCompressedFormat() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[1]{}; + + std::ostringstream out; + Error redirectError{&out}; + + CompressedPixelStorage storage; + CORRADE_VERIFY(!converter->convertToData(CompressedImageView2D{storage, 0, {1, 1}, bytes})); + CORRADE_COMPARE(out.str(), + "Trade::KtxImageConverter::convertToData(): implementation-specific formats are not supported\n"); +} + +void KtxImageConverterTest::dataFormatDescriptor() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[32]{}; + + constexpr PixelFormat start = PixelFormat::R8Unorm; + constexpr PixelFormat end = PixelFormat::Depth32FStencil8UI; + + for(UnsignedInt format = UnsignedInt(start); format <= UnsignedInt(end); ++format) { + CORRADE_ITERATION(format); + CORRADE_INTERNAL_ASSERT(Containers::arraySize(bytes) >= pixelSize(PixelFormat(format))); + const auto output = converter->convertToData(ImageView2D{PixelFormat(format), {1, 1}, bytes}); + CORRADE_VERIFY(output); + + const Implementation::KtxHeader& header = *reinterpret_cast(output.data()); + const Implementation::VkFormat vkFormat = Utility::Endianness::littleEndian(header.vkFormat); + + const auto dfd = readDataFormatDescriptor(output); + CORRADE_COMPARE(dfdMap.count(vkFormat), 1); + CORRADE_COMPARE_AS(dfd, dfdMap[vkFormat], TestSuite::Compare::Container); + } +} + +void KtxImageConverterTest::dataFormatDescriptorCompressed() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[32]{}; + + constexpr CompressedPixelFormat start = CompressedPixelFormat::Bc1RGBUnorm; + constexpr CompressedPixelFormat end = CompressedPixelFormat::PvrtcRGBA4bppSrgb; + + for(UnsignedInt format = UnsignedInt(start); format <= UnsignedInt(end); ++format) { + if(std::find(std::begin(UnsupportedCompressedFormats), std::end(UnsupportedCompressedFormats), + CompressedPixelFormat(format)) == std::end(UnsupportedCompressedFormats)) + { + CORRADE_ITERATION(format); + CORRADE_INTERNAL_ASSERT(Containers::arraySize(bytes) >= compressedBlockDataSize(CompressedPixelFormat(format))); + const auto output = converter->convertToData(CompressedImageView2D{CompressedPixelFormat(format), {1, 1}, bytes}); + CORRADE_VERIFY(output); + + const Implementation::KtxHeader& header = *reinterpret_cast(output.data()); + const Implementation::VkFormat vkFormat = Utility::Endianness::littleEndian(header.vkFormat); + + const auto dfd = readDataFormatDescriptor(output); + CORRADE_COMPARE(dfdMap.count(vkFormat), 1); + CORRADE_COMPARE_AS(dfd, dfdMap[vkFormat], TestSuite::Compare::Container); + } + } +} + +void KtxImageConverterTest::pixelStorage() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + constexpr UnsignedByte bytes[4*3]{ + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 9, 10, 11 + }; + + PixelStorage storage; + storage.setAlignment(4); + storage.setSkip({1, 1, 0}); + + const ImageView2D inputImage{storage, PixelFormat::R8UI, {2, 2}, Containers::arrayView(bytes)}; + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + if(_importerManager.loadState("KtxImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("KtxImporter plugin not found, cannot test"); + + Containers::Pointer importer = _importerManager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openData(output)); + + const auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE_AS(image->data(), Containers::arrayView({5, 6, 9, 10}), TestSuite::Compare::Container); +} + +void KtxImageConverterTest::tooManyLevels() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + 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::KtxImageConverter::convertToData(): there can be only 1 levels with base image size Vector(1, 1) but got 2\n"); +} + +void KtxImageConverterTest::levelWrongSize() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + 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::KtxImageConverter::convertToData(): expected size Vector(1, 1) for level 1 but got Vector(2, 1)\n"); +} + +void KtxImageConverterTest::convert1D() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + /* toktx writes no orientation for 1D files */ + converter->configuration().removeValue("orientation"); + converter->configuration().setValue("writerName", WriterToktx); + + const Color3ub data[4]{ + 0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x007f7f_rgb + }; + PixelStorage storage; + storage.setAlignment(1); + const ImageView1D inputImage{storage, PixelFormat::RGB8Srgb, {4}, data}; + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + /* Compare against 'ground truth' output generated by toktx/PVRTexTool */ + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert1DMipmaps() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().removeValue("orientation"); + converter->configuration().setValue("writerName", WriterToktx); + + constexpr Math::Vector<1, Int> size{4}; + const Color3ub mip0[4]{0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x007f7f_rgb}; + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Color3ub mip2[1]{0x000000_rgb}; + + PixelStorage storage; + storage.setAlignment(1); + const ImageView1D inputImages[3]{ + ImageView1D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 0, 1), mip0}, + ImageView1D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 1, 1), mip1}, + ImageView1D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 2, 1), mip2} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-mipmaps.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert1DCompressed() { + auto&& data = Convert1DCompressedData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "r"); + converter->configuration().setValue("writerName", WriterPVRTexTool); + + const auto blockData = Utility::Directory::read( + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::Directory::splitExtension(data.file).first + ".bin")); + const CompressedImageView1D inputImage{data.format, data.size, blockData}; + + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert1DCompressedMipmaps() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "r"); + converter->configuration().setValue("writerName", WriterPVRTexTool); + + constexpr Math::Vector<1, Int> size{7}; + const auto mip0 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-compressed-mipmaps-mip0.bin")); + const auto mip1 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-compressed-mipmaps-mip1.bin")); + const auto mip2 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-compressed-mipmaps-mip2.bin")); + + const CompressedImageView1D inputImages[3]{ + CompressedImageView1D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 0, 1), mip0}, + CompressedImageView1D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 1, 1), mip1}, + CompressedImageView1D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 2, 1), mip2} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-compressed-mipmaps.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert2D() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rd"); + converter->configuration().setValue("writerName", WriterToktx); + + PixelStorage storage; + storage.setAlignment(1); + const ImageView2D inputImage{storage, PixelFormat::RGB8Srgb, {4, 3}, PatternRgbData[Containers::arraySize(PatternRgbData) - 1]}; + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert2DMipmaps() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rd"); + converter->configuration().setValue("writerName", WriterToktx); + + constexpr Vector2i size{4, 3}; + const auto mip0 = Containers::arrayCast(Containers::arrayView( + PatternRgbData[Containers::arraySize(PatternRgbData) - 1])); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Color3ub mip2[1]{0x000000_rgb}; + + PixelStorage storage; + storage.setAlignment(1); + const ImageView2D inputImages[3]{ + ImageView2D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 0, 1), mip0}, + ImageView2D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 1, 1), mip1}, + ImageView2D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 2, 1), mip2} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-mipmaps.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert2DMipmapsIncomplete() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rd"); + converter->configuration().setValue("writerName", WriterToktx); + + constexpr Vector2i size{4, 3}; + const auto mip0 = Containers::arrayCast(Containers::arrayView( + PatternRgbData[Containers::arraySize(PatternRgbData) - 1])); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + + PixelStorage storage; + storage.setAlignment(1); + const ImageView2D inputImages[2]{ + ImageView2D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 0, 1), mip0}, + ImageView2D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 1, 1), mip1} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-mipmaps-incomplete.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert2DCompressed() { + auto&& data = Convert2DCompressedData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rd"); + converter->configuration().setValue("writerName", WriterPVRTexTool); + + const auto blockData = Utility::Directory::read( + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::Directory::splitExtension(data.file).first + ".bin")); + const CompressedImageView2D inputImage{data.format, data.size, blockData}; + + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert2DCompressedMipmaps() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rd"); + converter->configuration().setValue("writerName", WriterPVRTexTool); + + constexpr Vector2i size{9, 10}; + const auto mip0 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-mipmaps-mip0.bin")); + const auto mip1 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-mipmaps-mip1.bin")); + const auto mip2 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-mipmaps-mip2.bin")); + const auto mip3 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-mipmaps-mip3.bin")); + + const CompressedImageView2D inputImages[4]{ + CompressedImageView2D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 0, 1), mip0}, + CompressedImageView2D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 1, 1), mip1}, + CompressedImageView2D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 2, 1), mip2}, + CompressedImageView2D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 3, 1), mip3} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-mipmaps.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert3D() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rdi"); + converter->configuration().setValue("writerName", WriterPVRTexTool); + + PixelStorage storage; + storage.setAlignment(1); + const ImageView3D inputImage{storage, PixelFormat::RGB8Srgb, {4, 3, 3}, PatternRgbData}; + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert3DMipmaps() { + /* Neither toktx nor PVRTexTool can create mipmapped 3D textures. We use + the converter to create our own test file for the importer and the + converter ground truth. At the very least it catches unexpected changes. + Save it by running the test with: + --save-diagnostic [path/to/KtxImporter/Test] */ + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rdi"); + + const Vector3i size{4, 3, 3}; + const auto mip0 = Containers::arrayCast(Containers::arrayView(PatternRgbData)); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Color3ub mip2[1]{0x000000_rgb}; + + PixelStorage storage; + storage.setAlignment(1); + const ImageView3D inputImages[3]{ + ImageView3D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 0, 1), mip0}, + ImageView3D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 1, 1), mip1}, + ImageView3D{storage, PixelFormat::RGB8Srgb, Math::max(size >> 2, 1), mip2} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + CORRADE_COMPARE_AS(std::string(output.data(), output.size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-mipmaps.ktx2"), + TestSuite::Compare::StringToFile); +} + +void KtxImageConverterTest::convert3DCompressed() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rdi"); + converter->configuration().setValue("writerName", WriterPVRTexTool); + + const auto blockData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed.bin")); + const CompressedImageView3D inputImage{CompressedPixelFormat::Etc2RGB8Srgb, {9, 10, 3}, blockData}; + + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed.ktx2")); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); +} + +void KtxImageConverterTest::convert3DCompressedMipmaps() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + converter->configuration().setValue("orientation", "rdi"); + + /* Same as convert3DMipmaps, we generate this file here because none of the + tools can do it. The other compressed .bin data is extracted from files + created by toktx/PVRTexTool. In this case we handishly created data from + existing 2D ETC2 data. Oh well, better than nothing until there's a + better way to generate these images. */ + + constexpr Vector3i size{9, 10, 5}; + const auto mip0 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps-mip0.bin")); + const auto mip1 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps-mip1.bin")); + const auto mip2 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps-mip2.bin")); + const auto mip3 = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps-mip3.bin")); + + const CompressedImageView3D inputImages[4]{ + CompressedImageView3D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 0, 1), mip0}, + CompressedImageView3D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 1, 1), mip1}, + CompressedImageView3D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 2, 1), mip2}, + CompressedImageView3D{CompressedPixelFormat::Etc2RGB8Srgb, Math::max(size >> 3, 1), mip3} + }; + + const auto output = converter->convertToData(inputImages); + CORRADE_VERIFY(output); + + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps.ktx2")); + CORRADE_COMPARE_AS(std::string(output.data(), output.size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps.ktx2"), + TestSuite::Compare::StringToFile); +} + +void KtxImageConverterTest::convertFormats() { + auto&& data = ConvertFormatsData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + if(data.orientation) + converter->configuration().setValue("orientation", data.orientation); + if(data.writer) + converter->configuration().setValue("writerName", data.writer); + + PixelStorage storage; + storage.setAlignment(1); + const ImageView2D inputImage{storage, data.format, {4, 3}, data.data}; + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + if(data.save) { + CORRADE_COMPARE_AS(std::string(output.data(), output.size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file), + TestSuite::Compare::StringToFile); + } else { + const auto expected = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + CORRADE_COMPARE_AS(output, expected, TestSuite::Compare::Container); + } +} + +void KtxImageConverterTest::pvrtcRgb() { + auto&& data = PvrtcRgbData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[16]{}; + const UnsignedInt dataSize = compressedBlockDataSize(data.inputFormat); + const Vector2i imageSize = {2, 2}; + CORRADE_INTERNAL_ASSERT(Containers::arraySize(bytes) >= dataSize); + CORRADE_INTERNAL_ASSERT((Vector3i{imageSize, 1}) <= compressedBlockSize(data.inputFormat)); + + const CompressedImageView2D inputImage{data.inputFormat, imageSize, Containers::arrayView(bytes).prefix(dataSize)}; + const auto output = converter->convertToData(inputImage); + CORRADE_VERIFY(output); + + if(_importerManager.loadState("KtxImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("KtxImporter plugin not found, cannot test"); + + Containers::Pointer importer = _importerManager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openData(output)); + + const auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), data.outputFormat); + CORRADE_COMPARE_AS(image->data(), inputImage.data(), TestSuite::Compare::Container); +} + +void KtxImageConverterTest::configurationOrientation() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + /* Default value */ + CORRADE_COMPARE(converter->configuration().value("orientation"), "ruo"); + CORRADE_VERIFY(converter->configuration().setValue("orientation", "ldo")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView3D{PixelFormat::RGBA8Unorm, {1, 1, 1}, bytes}); + CORRADE_VERIFY(data); + + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(keyValueData.contains("KTXorientation\0ldo\0"_s)); +} + +void KtxImageConverterTest::configurationOrientationLessDimensions() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + /* Orientation string is shortened to the number of dimensions, extra characters are ignored */ + CORRADE_VERIFY(converter->configuration().setValue("orientation", "rdxxx")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(keyValueData.contains("KTXorientation\0rd\0"_s)); +} + +void KtxImageConverterTest::configurationOrientationEmpty() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().setValue("orientation", "")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + /* Empty orientation isn't written to key/value data at all */ + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(!keyValueData.contains("KTXorientation"_s)); +} + +void KtxImageConverterTest::configurationOrientationInvalid() { + auto&& data = InvalidOrientationData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().setValue("orientation", data.value)); + + std::ostringstream out; + Error redirectError{&out}; + + const UnsignedByte bytes[4]{}; + CORRADE_VERIFY(!converter->convertToData(ImageView3D{PixelFormat::RGBA8Unorm, {1, 1, 1}, bytes})); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::KtxImageConverter::convertToData(): {}\n", data.message)); +} + +void KtxImageConverterTest::configurationSwizzle() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + /* Default value */ + CORRADE_COMPARE(converter->configuration().value("swizzle"), ""); + CORRADE_VERIFY(converter->configuration().setValue("swizzle", "rgba")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(keyValueData.contains("KTXswizzle\0rgba\0"_s)); +} + +void KtxImageConverterTest::configurationSwizzleEmpty() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + /* Swizzle is empty by default, tested in configurationSwizzle() */ + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + /* Empty swizzle isn't written to key/value data at all */ + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(!keyValueData.contains("KTXswizzle"_s)); +} + +void KtxImageConverterTest::configurationSwizzleInvalid() { + auto&& data = InvalidSwizzleData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().setValue("swizzle", data.value)); + + std::ostringstream out; + Error redirectError{&out}; + + const UnsignedByte bytes[4]{}; + CORRADE_VERIFY(!converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes})); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::KtxImageConverter::convertToData(): {}\n", data.message)); +} + +void KtxImageConverterTest::configurationWriterName() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + /* Default value */ + CORRADE_COMPARE(converter->configuration().value("writerName"), "Magnum KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().setValue("writerName", "KtxImageConverterTest&$%1234@\x02\n\r\t\x15!")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + /* Writer doesn't have to be null-terminated, don't test for \0 */ + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(keyValueData.contains("KTXwriter\0KtxImageConverterTest&$%1234@\x02\n\r\t\x15!"_s)); +} + +void KtxImageConverterTest::configurationWriterNameEmpty() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().setValue("writerName", "")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + /* Empty writer name isn't written to key/value data at all */ + const auto keyValueData = readKeyValueData(data); + CORRADE_VERIFY(!keyValueData.contains("KTXwriter"_s)); +} + +void KtxImageConverterTest::configurationEmpty() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().removeValue("writerName")); + CORRADE_VERIFY(converter->configuration().removeValue("swizzle")); + CORRADE_VERIFY(converter->configuration().removeValue("orientation")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + /* Key/value data should not be written if it only contains empty values */ + + const Implementation::KtxHeader& header = *reinterpret_cast(data.data()); + CORRADE_COMPARE(header.kvdByteOffset, 0); + CORRADE_COMPARE(header.kvdByteLength, 0); +} + +void KtxImageConverterTest::configurationSorted() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + CORRADE_VERIFY(converter->configuration().setValue("writerName", "x")); + CORRADE_VERIFY(converter->configuration().setValue("swizzle", "barg")); + CORRADE_VERIFY(converter->configuration().setValue("orientation", "rd")); + + const UnsignedByte bytes[4]{}; + const auto data = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data); + + const auto keyValueData = readKeyValueData(data); + const auto writerOffset = keyValueData.find("KTXwriter"_s); + const auto swizzleOffset = keyValueData.find("KTXswizzle"_s); + const auto orientationOffset = keyValueData.find("KTXorientation"_s); + + CORRADE_VERIFY(!writerOffset.isEmpty()); + CORRADE_VERIFY(!swizzleOffset.isEmpty()); + CORRADE_VERIFY(!orientationOffset.isEmpty()); + + /* Entries are sorted alphabetically */ + CORRADE_VERIFY(orientationOffset.begin() < swizzleOffset.begin()); + CORRADE_VERIFY(swizzleOffset.begin() < writerOffset.begin()); +} + +void KtxImageConverterTest::convertTwice() { + Containers::Pointer converter = _converterManager.instantiate("KtxImageConverter"); + + const UnsignedByte bytes[4]{}; + const auto data1 = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + CORRADE_VERIFY(data1); + const auto data2 = converter->convertToData(ImageView2D{PixelFormat::RGBA8Unorm, {1, 1}, bytes}); + + /* Shouldn't crash, output should be identical */ + CORRADE_COMPARE_AS(data1, data2, TestSuite::Compare::Container); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Trade::Test::KtxImageConverterTest) diff --git a/src/MagnumPlugins/KtxImageConverter/Test/README.md b/src/MagnumPlugins/KtxImageConverter/Test/README.md new file mode 100644 index 000000000..1dc687054 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/Test/README.md @@ -0,0 +1,15 @@ +Updating test files +=================== + +The file dfd-data.bin is created using a patch to `testbidirectionalmapping` from [dfdutils](https://github.com/KhronosGroup/dfdutils): + +```bash +git clone https://github.com/KhronosGroup/dfdutils.git +cd dfdutils +git checkout 659a739bf60bdcd730b2159d658fb22e805854c2 +git apply path/to/KtxImageConverter/Test/testbidirectionalmapping.c.patch +make testbidirectionalmapping +# if the above doesn't work, run gcc directly: +# gcc testbidirectionalmapping.c interpretdfd.c createdfd.c -o testbidirectionalmapping -I. -g -W -Wall -std=c99 -pedantic +./testbidirectionalmapping +``` diff --git a/src/MagnumPlugins/KtxImageConverter/Test/configure.h.cmake b/src/MagnumPlugins/KtxImageConverter/Test/configure.h.cmake new file mode 100644 index 000000000..facf5f6d6 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/Test/configure.h.cmake @@ -0,0 +1,30 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#cmakedefine KTXIMAGECONVERTER_PLUGIN_FILENAME "${KTXIMAGECONVERTER_PLUGIN_FILENAME}" +#cmakedefine KTXIMPORTER_PLUGIN_FILENAME "${KTXIMPORTER_PLUGIN_FILENAME}" +#define KTXIMPORTER_TEST_DIR "${KTXIMPORTER_TEST_DIR}" +#define KTXIMAGECONVERTER_TEST_DIR "${KTXIMAGECONVERTER_TEST_DIR}" diff --git a/src/MagnumPlugins/KtxImageConverter/Test/dfd-data.bin b/src/MagnumPlugins/KtxImageConverter/Test/dfd-data.bin new file mode 100644 index 000000000..22d191bc0 Binary files /dev/null and b/src/MagnumPlugins/KtxImageConverter/Test/dfd-data.bin differ diff --git a/src/MagnumPlugins/KtxImageConverter/Test/testbidirectionalmapping.c.patch b/src/MagnumPlugins/KtxImageConverter/Test/testbidirectionalmapping.c.patch new file mode 100644 index 000000000..2018115d2 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/Test/testbidirectionalmapping.c.patch @@ -0,0 +1,62 @@ +diff --git a/testbidirectionalmapping.c b/testbidirectionalmapping.c +index 480d103..8ab065b 100644 +--- a/testbidirectionalmapping.c ++++ b/testbidirectionalmapping.c +@@ -568,6 +568,14 @@ uint32_t *getmap(enum VkFormat f) + } + } + ++void appendmap(uint32_t format, FILE* file, uint32_t* dfd) ++{ ++ // assumes little-endian system ++ uint32_t dfdsize = dfd[0]; ++ fwrite(&format, sizeof(format), 1, file); ++ fwrite(dfd, dfdsize, 1, file); ++} ++ + enum VkFormat unmap(uint32_t *dfd) + { + #include "dfd2vk.inl" +@@ -575,12 +583,15 @@ enum VkFormat unmap(uint32_t *dfd) + + int main() + { ++ FILE *file = fopen("dfd-data.bin", "wb"); ++ + unsigned int i; + for (i = 1; i <= 184; ++i) { + uint32_t *dfd = getmap((enum VkFormat)i); + VkFormat f = unmap(dfd); + if (i != f) printf("Input and output enums differ: %s (%d) -> %s (%d)\n", + formatname(i),i, formatname(f),f); ++ else appendmap(i, file, dfd); + free((void *)dfd); + } + +@@ -591,6 +602,7 @@ int main() + VkFormat f = unmap(dfd); + if (i != f) printf("Input and output enums differ: %s (%d) -> %s (%d)\n", + formatname(i),i, formatname(f),f); ++ else appendmap(i, file, dfd); + free((void *)dfd); + } + +@@ -600,6 +612,7 @@ int main() + VkFormat f = unmap(dfd); + if (i != f) printf("Input and output enums differ: %s (%d) -> %s (%d)\n", + formatname(i),i, formatname(f),f); ++ else appendmap(i, file, dfd); + free((void *)dfd); + } + +@@ -609,7 +622,10 @@ int main() + VkFormat f = unmap(dfd); + if (i != f) printf("Input and output enums differ: %s (%d) -> %s (%d)\n", + formatname(i),i, formatname(f),f); ++ else appendmap(i, file, dfd); + free((void *)dfd); + } ++ ++ fclose(file); + return 0; + } diff --git a/src/MagnumPlugins/KtxImageConverter/configure.h.cmake b/src/MagnumPlugins/KtxImageConverter/configure.h.cmake new file mode 100644 index 000000000..d07728d96 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/configure.h.cmake @@ -0,0 +1,27 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#cmakedefine MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC diff --git a/src/MagnumPlugins/KtxImageConverter/importStaticPlugin.cpp b/src/MagnumPlugins/KtxImageConverter/importStaticPlugin.cpp new file mode 100644 index 000000000..d07298524 --- /dev/null +++ b/src/MagnumPlugins/KtxImageConverter/importStaticPlugin.cpp @@ -0,0 +1,36 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "MagnumPlugins/KtxImageConverter/configure.h" + +#ifdef MAGNUM_KTXIMAGECONVERTER_BUILD_STATIC +#include + +static int magnumKtxImageConverterStaticImporter() { + CORRADE_PLUGIN_IMPORT(KtxImageConverter) + return 1; +} CORRADE_AUTOMATIC_INITIALIZER(magnumKtxImageConverterStaticImporter) +#endif diff --git a/src/MagnumPlugins/KtxImporter/CMakeLists.txt b/src/MagnumPlugins/KtxImporter/CMakeLists.txt new file mode 100644 index 000000000..69c382e9d --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/CMakeLists.txt @@ -0,0 +1,74 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# 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"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +find_package(Magnum REQUIRED Trade) + +if(BUILD_PLUGINS_STATIC AND NOT DEFINED MAGNUM_KTXIMPORTER_BUILD_STATIC) + set(MAGNUM_KTXIMPORTER_BUILD_STATIC 1) +endif() + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + +# KtxImporter plugin +add_plugin(KtxImporter + "${MAGNUM_PLUGINS_IMPORTER_DEBUG_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_IMPORTER_DEBUG_LIBRARY_INSTALL_DIR}" + "${MAGNUM_PLUGINS_IMPORTER_RELEASE_BINARY_INSTALL_DIR};${MAGNUM_PLUGINS_IMPORTER_RELEASE_LIBRARY_INSTALL_DIR}" + KtxImporter.conf + KtxImporter.cpp + KtxImporter.h + KtxHeader.h + formatMapping.hpp) +if(MAGNUM_KTXIMPORTER_BUILD_STATIC AND BUILD_STATIC_PIC) + set_target_properties(KtxImporter PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif() +target_include_directories(KtxImporter PUBLIC + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_BINARY_DIR}/src) +target_link_libraries(KtxImporter PUBLIC Magnum::Trade) +# Modify output location only if all are set, otherwise it makes no sense +if(CMAKE_RUNTIME_OUTPUT_DIRECTORY AND CMAKE_LIBRARY_OUTPUT_DIRECTORY AND CMAKE_ARCHIVE_OUTPUT_DIRECTORY) + set_target_properties(KtxImporter PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/magnum$<$:-d>/importers + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/magnum$<$:-d>/importers + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/magnum$<$:-d>/importers) +endif() + +install(FILES KtxImporter.h ${CMAKE_CURRENT_BINARY_DIR}/configure.h + DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/KtxImporter) + +# Automatic static plugin import +if(MAGNUM_KTXIMPORTER_BUILD_STATIC) + install(FILES importStaticPlugin.cpp DESTINATION ${MAGNUM_PLUGINS_INCLUDE_INSTALL_DIR}/KtxImporter) + target_sources(KtxImporter INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/importStaticPlugin.cpp) +endif() + +if(BUILD_TESTS) + add_subdirectory(Test) +endif() + +# MagnumPlugins KtxImporter target alias for superprojects +add_library(MagnumPlugins::KtxImporter ALIAS KtxImporter) diff --git a/src/MagnumPlugins/KtxImporter/KtxHeader.h b/src/MagnumPlugins/KtxImporter/KtxHeader.h new file mode 100644 index 000000000..ea9f083e9 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/KtxHeader.h @@ -0,0 +1,227 @@ +#ifndef Magnum_Trade_KtxHeader_h +#define Magnum_Trade_KtxHeader_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include + +/* Used by both KtxImporter and KtxImageConverter, which is why it isn't + directly inside KtxImporter.cpp. OTOH it doesn't need to be exposed + publicly, which is why it has no docblocks. */ + +namespace Magnum { namespace Trade { namespace Implementation { + +typedef UnsignedInt VkFormat; + +/* Selected Vulkan 1.0 formats for detecting implicit swizzling to PixelFormat. + VkFormat is UnsignedInt instead of this enum to prevent warnings when using + arbitrary numeric values from formatMapping.hpp in switches. */ +enum : UnsignedInt { + VK_FORMAT_UNDEFINED = 0, + VK_FORMAT_B8G8R8_UNORM = 30, + VK_FORMAT_B8G8R8_SNORM = 31, + VK_FORMAT_B8G8R8_UINT = 34, + VK_FORMAT_B8G8R8_SINT = 35, + VK_FORMAT_B8G8R8_SRGB = 36, + VK_FORMAT_B8G8R8A8_UNORM = 44, + VK_FORMAT_B8G8R8A8_SNORM = 45, + VK_FORMAT_B8G8R8A8_UINT = 48, + VK_FORMAT_B8G8R8A8_SINT = 49, + VK_FORMAT_B8G8R8A8_SRGB = 50 +}; + +enum VkFormatSuffix : UnsignedByte { + UNORM = 1, + SNORM, + UINT, + SINT, + UFLOAT, + SFLOAT, + SRGB + /* SCALED formats are not allowed by KTX, and not exposed by Magnum, either. + They're usually used as vertex formats. */ +}; + +enum SuperCompressionScheme : UnsignedInt { + None = 0, + BasisLZ = 1, + Zstandard = 2, + ZLIB = 3 +}; + +/* KTX2 file header */ +struct KtxHeader { + char identifier[12]; /* File identifier */ + VkFormat vkFormat; /* Texel format, VK_FORMAT_UNDEFINED = custom */ + UnsignedInt typeSize; /* Size of channel data type, in bytes */ + Vector3ui imageSize; /* Image level 0 size */ + UnsignedInt layerCount; /* Number of array elements */ + UnsignedInt faceCount; /* Number of cubemap faces */ + UnsignedInt levelCount; /* Number of mip levels */ + SuperCompressionScheme supercompressionScheme; + /* Index */ + UnsignedInt dfdByteOffset; /* Offset of Data Format Descriptor */ + UnsignedInt dfdByteLength; /* Length of Data Format Descriptor */ + UnsignedInt kvdByteOffset; /* Offset of Key/Value Data */ + UnsignedInt kvdByteLength; /* Length of Key/Value Data */ + UnsignedLong sgdByteOffset; /* Offset of Supercompression Global Data */ + UnsignedLong sgdByteLength; /* Length of Supercompression Global Data */ +}; + +static_assert(sizeof(KtxHeader) == 80, "Improper size of KtxHeader struct"); + +/* KTX2 mip level index element */ +struct KtxLevel { + UnsignedLong byteOffset; /* Offset of first byte of image data */ + UnsignedLong byteLength; /* Total size of image data */ + UnsignedLong uncompressedByteLength; /* Total size of image data before supercompression */ +}; + +static_assert(sizeof(KtxLevel) == 24, "Improper size of KtxLevel struct"); + +constexpr char KtxFileIdentifier[12]{ + /* https://github.khronos.org/KTX-Specification/#_identifier */ + '\xab', 'K', 'T', 'X', ' ', '2', '0', '\xbb', '\r', '\n', '\x1a', '\n' +}; + +static_assert(sizeof(KtxFileIdentifier) == sizeof(KtxHeader::identifier), "Improper size of KtxFileIdentifier data"); + +constexpr std::size_t KtxFileVersionOffset = 5; +constexpr std::size_t KtxFileVersionLength = 2; +static_assert(KtxFileVersionOffset + KtxFileVersionLength <= sizeof(KtxFileIdentifier), "KtxFileVersion(Offset|Length) out of bounds"); + +/* Khronos Data Format: basic block header */ +struct KdfBasicBlockHeader { + enum class VendorId : UnsignedShort { + Khronos = 0 + }; + + enum class DescriptorType : UnsignedShort { + Basic = 0 + }; + + enum class VersionNumber : UnsignedShort { + Kdf1_3 = 2 + }; + + enum class ColorModel : UnsignedByte { + /* Uncompressed formats. There are a lot more, but KTX doesn't allow + those. */ + Rgbsda = 1, /* Additive colors: red, green, blue, stencil, depth, alpha */ + + /* Compressed formats, each one has its own color model */ + Bc1 = 128, /* DXT1 */ + Bc2 = 129, /* DXT2/3 */ + Bc3 = 130, /* DXT4/5 */ + Bc4 = 131, + Bc5 = 132, + Bc6h = 133, + Bc7 = 134, + Etc1 = 160, + Etc2 = 161, + Astc = 162, + Etc1s = 163, + Pvrtc = 164, + Pvrtc2 = 165, + + /* Basis Universal */ + BasisUastc = 166, + BasisEtc1s = Etc1s + }; + + enum class ColorPrimaries : UnsignedByte { + /* We have no way to guess color space, this is the recommended default */ + Srgb = 1 /* BT.709 */ + }; + + enum class TransferFunction : UnsignedByte { + /* There are a lot more, but KTX doesn't allow those */ + Linear = 1, + Srgb = 2 + }; + + enum Flags : UnsignedByte { + AlphaPremultiplied = 1 + }; + + /* Technically, the first two members are 17 and 15 bits, but bit fields + aren't very portable. We only check for values 0/0 so this works for our + use case. */ + VendorId vendorId; + DescriptorType descriptorType; + VersionNumber versionNumber; + UnsignedShort descriptorBlockSize; + + ColorModel colorModel; + ColorPrimaries colorPrimaries; + TransferFunction transferFunction; + Flags flags; + UnsignedByte texelBlockDimension[4]; + UnsignedByte bytesPlane[8]; +}; + +static_assert(sizeof(KdfBasicBlockHeader) == 24, "Improper size of KdfBasicBlockHeader struct"); + +/* Khronos Data Format: Basic block sample element, one for each color channel */ +struct KdfBasicBlockSample { + /* Channel id encoded in lower half of channelType */ + enum ChannelId : UnsignedByte { + /* ColorModel::Rgbsda */ + Red = 0, + Green = 1, + Blue = 2, + Stencil = 13, + Depth = 14, + Alpha = 15, + /* Compressed color models. Some use Red/Green/Alpha from Rgbsda if + applicable. */ + Color = 0, + Bc1Alpha = 1, + Etc2Color = 2 + }; + + /* Channel data type bit mask encoded in upper half of channelType */ + enum ChannelFormat : UnsignedByte { + Linear = 1 << (4 + 0), /* Ignore the transfer function */ + Exponent = 1 << (4 + 1), + Signed = 1 << (4 + 2), + Float = 1 << (4 + 3) + }; + + UnsignedShort bitOffset; + UnsignedByte bitLength; /* Length - 1 */ + UnsignedByte channelType; + UnsignedByte position[4]; + UnsignedInt lower; + UnsignedInt upper; +}; + +static_assert(sizeof(KdfBasicBlockSample) == 16, "Improper size of KdfBasicBlockSample struct"); + +}}} + +#endif diff --git a/src/MagnumPlugins/KtxImporter/KtxImporter.conf b/src/MagnumPlugins/KtxImporter/KtxImporter.conf new file mode 100644 index 000000000..e69de29bb diff --git a/src/MagnumPlugins/KtxImporter/KtxImporter.cpp b/src/MagnumPlugins/KtxImporter/KtxImporter.cpp new file mode 100644 index 000000000..664dff4ec --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/KtxImporter.cpp @@ -0,0 +1,802 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "KtxImporter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "MagnumPlugins/KtxImporter/KtxHeader.h" + +namespace Magnum { namespace Trade { + +namespace { + +template struct TypeForSize {}; +template<> struct TypeForSize<1> { typedef UnsignedByte Type; }; +template<> struct TypeForSize<2> { typedef UnsignedShort Type; }; +template<> struct TypeForSize<4> { typedef UnsignedInt Type; }; +template<> struct TypeForSize<8> { typedef UnsignedLong Type; }; + +/** @todo Can we perform endian-swap together with the swizzle? Might get messy + and it'll be untested... */ +void endianSwap(Containers::ArrayView data, UnsignedInt typeSize) { + switch(typeSize) { + case 1: + /* Single-byte or block-compressed format, nothing to do */ + return; + case 2: + Utility::Endianness::littleEndianInPlace(Containers::arrayCast::Type>(data)); + return; + case 4: + Utility::Endianness::littleEndianInPlace(Containers::arrayCast::Type>(data)); + return; + case 8: + Utility::Endianness::littleEndianInPlace(Containers::arrayCast::Type>(data)); + return; + } + + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +enum SwizzleType : UnsignedByte { + None = 0, + BGR, + BGRA +}; + +inline SwizzleType& operator ^=(SwizzleType& a, SwizzleType b) { + /* This is meant to toggle single enum values, make sure it's not being + used for other bit-fiddling crimes */ + CORRADE_INTERNAL_ASSERT(a == SwizzleType::None || a == b); + return a = SwizzleType(a ^ b); +} + +template void swizzlePixels(SwizzleType type, Containers::ArrayView data) { + if(type == SwizzleType::BGR) { + for(auto& pixel: Containers::arrayCast>(data)) + pixel = Math::gather<'b', 'g', 'r'>(pixel); + } else if(type == SwizzleType::BGRA) { + for(auto& pixel: Containers::arrayCast>(data)) + pixel = Math::gather<'b', 'g', 'r', 'a'>(pixel); + } +} + +void swizzlePixels(SwizzleType type, UnsignedInt typeSize, Containers::ArrayView data) { + switch(typeSize) { + case 1: + swizzlePixels::Type>(type, data); + return; + case 2: + swizzlePixels::Type>(type, data); + return; + case 4: + swizzlePixels::Type>(type, data); + return; + case 8: + swizzlePixels::Type>(type, data); + return; + } + + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +struct Format { + union { + PixelFormat uncompressed; + CompressedPixelFormat compressed; + }; + + bool isCompressed; + bool isDepth; + + /* Size of entire pixel/block */ + UnsignedInt size; + /* Compressed block size, (1,1,1) for uncompressed formats */ + Vector3i blockSize; + /* Size of underlying data type, 1 for block-compressed formats */ + UnsignedInt typeSize; + + SwizzleType swizzle; +}; + +Containers::Optional decodeFormat(Implementation::VkFormat vkFormat) { + Format f{}; + + /* Find uncompressed pixel format. Note that none of the formats forbidden + by KTX are supported by Magnum, so we filter those at the same time. + This might change in the future, but we'll be fine as long as + formatMapping.hpp isn't updated without adding an extra check. */ + PixelFormat format{}; + switch(vkFormat) { + #define _c(vulkan, magnum, _type) case vulkan: format = PixelFormat::magnum; break; + #include "MagnumPlugins/KtxImporter/formatMapping.hpp" + #undef _c + default: + break; + } + + /* PixelFormat doesn't contain any of the swizzled formats. Figure it out + from the Vulkan format and remember that we need to swizzle in doImage(). */ + if(format == PixelFormat{}) { + switch(vkFormat) { + case Implementation::VK_FORMAT_B8G8R8_UNORM: format = PixelFormat::RGB8Unorm; break; + case Implementation::VK_FORMAT_B8G8R8_SNORM: format = PixelFormat::RGB8Snorm; break; + case Implementation::VK_FORMAT_B8G8R8_UINT: format = PixelFormat::RGB8UI; break; + case Implementation::VK_FORMAT_B8G8R8_SINT: format = PixelFormat::RGB8I; break; + case Implementation::VK_FORMAT_B8G8R8_SRGB: format = PixelFormat::RGB8Srgb; break; + case Implementation::VK_FORMAT_B8G8R8A8_UNORM: format = PixelFormat::RGBA8Unorm; break; + case Implementation::VK_FORMAT_B8G8R8A8_SNORM: format = PixelFormat::RGBA8Snorm; break; + case Implementation::VK_FORMAT_B8G8R8A8_UINT: format = PixelFormat::RGBA8UI; break; + case Implementation::VK_FORMAT_B8G8R8A8_SINT: format = PixelFormat::RGBA8I; break; + case Implementation::VK_FORMAT_B8G8R8A8_SRGB: format = PixelFormat::RGBA8Srgb; break; + default: + break; + } + + if(format != PixelFormat{}) { + f.size = pixelSize(format); + CORRADE_INTERNAL_ASSERT(f.size == 3 || f.size == 4); + f.swizzle = (f.size == 3) ? SwizzleType::BGR : SwizzleType::BGRA; + } + } + + if(format != PixelFormat{}) { + /* Depth formats are allowed by KTX. We only really use isDepth for + validation. */ + switch(format) { + case PixelFormat::Depth16Unorm: + case PixelFormat::Depth24Unorm: + case PixelFormat::Depth32F: + case PixelFormat::Stencil8UI: + case PixelFormat::Depth16UnormStencil8UI: + case PixelFormat::Depth24UnormStencil8UI: + case PixelFormat::Depth32FStencil8UI: + f.isDepth = true; + break; + default: + /* PixelFormat covers all of Vulkan's depth formats */ + break; + } + + f.size = pixelSize(format); + f.blockSize = {1, 1, 1}; + f.uncompressed = format; + return f; + } + + /* Find block-compressed pixel format, no swizzling possible */ + /** @todo KTX supports 3D ASTC formats through an unreleased extension. + Supposedly the enum values won't change so we could manually map + them, although it'd be easier if Magnum did this. + See https://github.com/KhronosGroup/KTX-Specification/pull/97 and + https://github.com/KhronosGroup/KTX-Software/blob/f99221eb1c5ad92fd859765a0c66517ea4059160/lib/dfdutils/vulkan/vulkan_core.h#L1061*/ + CompressedPixelFormat compressedFormat{}; + switch(vkFormat) { + #define _c(vulkan, magnum, _type) case vulkan: compressedFormat = CompressedPixelFormat::magnum; break; + #include "MagnumPlugins/KtxImporter/compressedFormatMapping.hpp" + #undef _c + default: + break; + } + + if(compressedFormat != CompressedPixelFormat{}) { + f.size = compressedBlockDataSize(compressedFormat); + f.blockSize = compressedBlockSize(compressedFormat); + f.compressed = compressedFormat; + f.isCompressed = true; + return f; + } + + /** @todo Support all Vulkan formats allowed by the KTX spec. Create custom + PixelFormat with pixelFormatWrap and manually fill PixelStorage/ + CompressedPixelStorage. We can take all the necessary info from + https://github.com/KhronosGroup/KTX-Specification/blob/master/formats.json + Do we also need this for the KtxImageConverter? This would allow + users to pass in images with implementation-specific PixelFormat + using the Vulkan format enum directly. + Is this actually worth the effort? Which Vulkan formats are not + supported by PixelFormat? */ + + return {}; +} + +} + +struct KtxImporter::File { + struct LevelData { + Vector3i size; + Containers::ArrayView data; + }; + + Containers::Array in; + + /* Dimensions of the source image (1-3) */ + UnsignedByte numDimensions; + /* Dimensions of the imported image data, including extra dimensions for + array layers or cube map faces */ + UnsignedByte numDataDimensions; + TextureType type; + BoolVector3 flip; + + Format pixelFormat; + + /* Usually only one image with n or n+1 dimensions, multiple images for + 3D array layers */ + Containers::Array> imageData; +}; + +KtxImporter::KtxImporter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImporter{manager, plugin} {} + +KtxImporter::~KtxImporter() = default; + +ImporterFeatures KtxImporter::doFeatures() const { return ImporterFeature::OpenData; } + +bool KtxImporter::doIsOpened() const { return !!_f; } + +void KtxImporter::doClose() { _f = nullptr; } + +void KtxImporter::doOpenData(const Containers::ArrayView data) { + /* Check if the file is long enough for the header */ + if(data.size() < sizeof(Implementation::KtxHeader)) { + Error{} << "Trade::KtxImporter::openData(): file too short, expected" << + sizeof(Implementation::KtxHeader) << "bytes for the header but got only" << data.size(); + return; + } + + Implementation::KtxHeader header = *reinterpret_cast(data.data()); + + /* KTX2 uses little-endian everywhere */ + Utility::Endianness::littleEndianInPlace( + header.vkFormat, header.typeSize, + header.imageSize[0], header.imageSize[1], header.imageSize[2], + header.layerCount, header.faceCount, header.levelCount, + header.supercompressionScheme, + header.kvdByteOffset, header.kvdByteLength); + + using namespace Containers::Literals; + + /* Check magic string */ + const Containers::StringView identifier{header.identifier, sizeof(header.identifier)}; + const Containers::StringView expected{Implementation::KtxFileIdentifier, sizeof(header.identifier)}; + if(identifier != expected) { + /* Print a useful error for a KTX file with an unsupported version. + KTX1 uses the same magic string but with a different version string. */ + if(identifier.hasPrefix(expected.prefix(Implementation::KtxFileVersionOffset))) { + const auto version = identifier.suffix(Implementation::KtxFileVersionOffset).prefix(Implementation::KtxFileVersionLength); + if(version != "20"_s) { + Error() << "Trade::KtxImporter::openData(): unsupported KTX version, expected 20 but got" << version; + return; + } + } + + Error() << "Trade::KtxImporter::openData(): wrong file signature"; + return; + } + + /* Read header data and perform some sanity checks, including byte ranges */ + + /** @todo Support Basis compression */ + if(header.vkFormat == Implementation::VK_FORMAT_UNDEFINED) { + Error{} << "Trade::KtxImporter::openData(): custom formats are not supported"; + return; + } + + /** @todo Support supercompression */ + if(header.supercompressionScheme != Implementation::SuperCompressionScheme::None) { + Error{} << "Trade::KtxImporter::openData(): supercompression is currently not supported"; + return; + } + + /* typeSize is the size of the format's underlying type, not the texel + size, e.g. 2 for RG16F. For any sane format it should be a + power-of-two between 1 and 8. */ + if(header.typeSize < 1 || header.typeSize > 8 || + (header.typeSize & (header.typeSize - 1))) + { + Error{} << "Trade::KtxImporter::openData(): unsupported type size" << header.typeSize; + return; + } + + if(header.imageSize.x() == 0) { + Error{} << "Trade::KtxImporter::openData(): invalid image size, width is 0"; + return; + } + + Containers::Pointer f{InPlaceInit}; + + /* Number of array layers, imported as extra image dimensions (except + for 3D images, there it's one Image3D per layer). + + layerCount == 1 is an array image with one level, we export it as such + so that there are no surprises. This is equivalent to how we handle + depth == 1. */ + const bool isLayered = header.layerCount > 0; + const UnsignedInt numLayers = Math::max(header.layerCount, 1u); + + const bool isCubeMap = header.faceCount == 6; + const UnsignedInt numFaces = header.faceCount; + + /* Get image dimensions and remember the type for doTexture() */ + if(header.imageSize.y() > 0) { + if(header.imageSize.z() > 0) { + f->numDimensions = 3; + f->type = TextureType::Texture3D; + } else { + f->numDimensions = 2; + if(isCubeMap) + f->type = isLayered ? TextureType::CubeMapArray : TextureType::CubeMap; + else + f->type = isLayered ? TextureType::Texture2DArray : TextureType::Texture2D; + } + } else if(header.imageSize.z() > 0) { + Error{} << "Trade::KtxImporter::openData(): invalid image size, depth is" << header.imageSize.z() << "but height is 0"; + return; + } else { + f->numDimensions = 1; + f->type = isLayered ? TextureType::Texture1DArray : TextureType::Texture1D; + } + + f->numDataDimensions = Math::min(f->numDimensions + UnsignedByte(isLayered || isCubeMap), 3); + + CORRADE_INTERNAL_ASSERT(f->numDimensions >= 1 && f->numDimensions <= 3); + CORRADE_INTERNAL_ASSERT(f->numDataDimensions >= f->numDimensions); + CORRADE_INTERNAL_ASSERT(f->numDataDimensions - f->numDimensions <= 1); + + /* Make size 1 instead of 0 in missing dimensions for simpler calculations */ + const Vector3i size = Math::max(Vector3i{header.imageSize}, 1); + + if(numFaces != 1) { + if(numFaces != 6) { + Error{} << "Trade::KtxImporter::openData(): expected either 1 or 6 faces for cube maps but got" << numFaces; + return; + } + + if(f->numDimensions != 2 || size.x() != size.y()) { + Error{} << "Trade::KtxImporter::openData(): cube map dimensions must be 2D and square, but got" << header.imageSize; + return; + } + } + + /* levelCount == 0 indicates to the user/importer to generate mipmaps. We + don't really care either way since we don't generate mipmaps or pass + this on to the user. */ + const UnsignedInt numMipmaps = Math::max(header.levelCount, 1u); + + const UnsignedInt maxLevelCount = Math::log2(size.max()) + 1; + if(numMipmaps > maxLevelCount) { + Error{} << "Trade::KtxImporter::openData(): expected at most" << + maxLevelCount << "mip levels but got" << numMipmaps; + return; + } + + const std::size_t levelIndexEnd = sizeof(header) + numMipmaps*sizeof(Implementation::KtxLevel); + if(data.size() < levelIndexEnd) { + Error{} << "Trade::KtxImporter::openData(): file too short, expected" << + levelIndexEnd << "bytes for level index but got only" << data.size(); + return; + } + + const std::size_t kvdEnd = header.kvdByteOffset + header.kvdByteLength; + if(data.size() < kvdEnd) { + Error{} << "Trade::KtxImporter::openData(): file too short, expected" << + kvdEnd << "bytes for key/value data but got only" << data.size(); + return; + } + + /* Get generic format info from Vulkan format */ + const auto pixelFormat = decodeFormat(header.vkFormat); + if(!pixelFormat) { + Error{} << "Trade::KtxImporter::openData(): unsupported format" << header.vkFormat; + return; + } + f->pixelFormat = *pixelFormat; + + if(f->pixelFormat.isDepth && f->numDimensions == 3) { + Error{} << "Trade::KtxImporter::openData(): 3D images can't have depth/stencil format"; + return; + } + + /* Block-compressed formats have no implicit swizzle */ + CORRADE_INTERNAL_ASSERT(!f->pixelFormat.isCompressed || f->pixelFormat.swizzle == SwizzleType::None); + + if(f->pixelFormat.isCompressed && header.typeSize != 1) { + Error{} << "Trade::KtxImporter::openData(): invalid type size for compressed format, expected 1 but got" << header.typeSize; + return; + } + f->pixelFormat.typeSize = header.typeSize; + + /* Make an owned copy of the data */ + f->in = Containers::Array{NoInit, data.size()}; + Utility::copy(data, f->in); + + /* The level index contains byte ranges for each mipmap, from largest to + smallest. Each mipmap contains tightly packed images ordered by + layers, faces/slices, rows, columns. */ + const std::size_t levelIndexSize = numMipmaps*sizeof(Implementation::KtxLevel); + const auto levelIndex = Containers::arrayCast( + f->in.suffix(sizeof(Implementation::KtxHeader)).prefix(levelIndexSize)); + + /* Extract image data views. Only one image with extra dimensions for array + layers and/or cube map faces, except for 3D array images where it's one + image per layer. */ + const bool is3DArrayImage = f->numDimensions == 3 && isLayered; + const UnsignedInt numImages = is3DArrayImage ? numLayers : 1; + f->imageData = Containers::Array>{numImages}; + for(UnsignedInt image = 0; image != numImages; ++image) + f->imageData[image] = Containers::Array{numMipmaps}; + + Vector3i mipSize{size}; + for(UnsignedInt i = 0; i != numMipmaps; ++i) { + auto& level = levelIndex[i]; + Utility::Endianness::littleEndianInPlace(level.byteOffset, + level.byteLength, level.uncompressedByteLength); + + /* Both lengths should be equal without supercompression. Be lenient here + and only emit a warning in case some shitty exporter gets this wrong. */ + if(header.supercompressionScheme == Implementation::SuperCompressionScheme::None && + level.byteLength != level.uncompressedByteLength) + { + Warning{} << "Trade::KtxImporter::openData(): byte length" << level.byteLength + << "is not equal to uncompressed byte length" << level.uncompressedByteLength + << "for an image without supercompression, ignoring the latter"; + } + + const std::size_t levelEnd = level.byteOffset + level.byteLength; + if(data.size() < levelEnd) { + Error{} << "Trade::KtxImporter::openData(): file too short, expected" + << levelEnd << "bytes for level data but got only" << data.size(); + return; + } + + Vector3i levelSize = mipSize; + if(f->numDimensions < f->numDataDimensions) { + CORRADE_INTERNAL_ASSERT(!is3DArrayImage); + CORRADE_INTERNAL_ASSERT(levelSize[f->numDimensions] == 1); + levelSize[f->numDimensions] = numFaces*numLayers; + } + + std::size_t imageLength; + if(f->pixelFormat.isCompressed) { + const Vector3i& blockSize = f->pixelFormat.blockSize; + const Vector3i blockCount = (levelSize + (blockSize - Vector3i{1}))/blockSize; + imageLength = blockCount.product()*f->pixelFormat.size; + } else + imageLength = levelSize.product()*f->pixelFormat.size; + const std::size_t totalLength = imageLength*numImages; + + if(level.byteLength < totalLength) { + Error{} << "Trade::KtxImporter::openData(): level data too short, " + "expected at least" << totalLength << "bytes but got" << level.byteLength; + return; + } + + for(UnsignedInt image = 0; image != numImages; ++image) { + const std::size_t offset = level.byteOffset + image*imageLength; + f->imageData[image][i] = {levelSize, f->in.suffix(offset).prefix(imageLength)}; + } + + /* Halve each dimension, rounding down */ + mipSize = Math::max(mipSize >> 1, 1); + } + + /* Read key/value data, optional */ + + enum KeyValueType : UnsignedByte { + CubeMapIncomplete, + Orientation, + Swizzle, + Count + }; + + struct KeyValueEntry { + Containers::StringView key; + Containers::ArrayView value; + } keyValueEntries[KeyValueType::Count]{ + {"KTXcubemapIncomplete"_s, {}}, + {"KTXorientation"_s, {}}, + {"KTXswizzle"_s, {}}, + }; + + if(header.kvdByteLength > 0) { + Containers::ArrayView keyValueData{f->in.suffix(header.kvdByteOffset).prefix(header.kvdByteLength)}; + /* Loop through entries, each one consisting of: + + UnsignedInt length + Byte data[length] + Byte padding[...] + + data[] begins with a zero-terminated key, the rest of the bytes is + the value content. Value alignment must be implicitly done through + key length, hence the funny KTX keys with multiple underscores. Any + multi-byte numbers in values must be endian-swapped later. */ + UnsignedInt current = 0; + while(current + sizeof(UnsignedInt) < keyValueData.size()) { + /* Length without padding */ + const UnsignedInt length = *reinterpret_cast(keyValueData.suffix(current).data()); + Utility::Endianness::littleEndianInPlace(length); + current += sizeof(length); + + if(current + length <= keyValueData.size()) { + const Containers::StringView entry{keyValueData.suffix(current).prefix(length)}; + const Containers::Array3 split = entry.partition('\0'); + const auto key = split[0]; + const auto value = split[2]; + + if(key.isEmpty()) + Warning{} << "Trade::KtxImporter::openData(): invalid key/value entry, skipping"; + else { + for(UnsignedInt i = 0; i != Containers::arraySize(keyValueEntries); ++i) { + if(key == keyValueEntries[i].key) { + if(!keyValueEntries[i].value.empty()) + Warning{} << "Trade::KtxImporter::openData(): key" << key << "already set, skipping"; + else + keyValueEntries[i].value = value; + break; + } + } + } + } + /* Length value is dword-aligned, guaranteed for the first length + by the file layout */ + current += (length + 3)/4*4; + } + } + + /* Read image orientation so we can flip if needed. + + l/r = left/right + u/d = up/down + o/i = out of/into screen + + The spec strongly recommends defaulting to rdi, Magnum/GL expects ruo. */ + { + constexpr auto targetOrientation = "ruo"_s; + + bool useDefaultOrientation = true; + const Containers::StringView orientation{keyValueEntries[KeyValueType::Orientation].value}; + /* If the orientation string is too short or invalid, a warning gets + printed and the default is used. + Strings that are null-terminated but too short pass the first if + but get caught by the test for valid orientation characters. */ + if(orientation.size() >= f->numDimensions) { + constexpr Containers::StringView validOrientations[3]{"rl"_s, "du"_s, "io"_s}; + for(UnsignedByte i = 0; i != f->numDimensions; ++i) { + useDefaultOrientation = !validOrientations[i].contains(orientation[i]); + if(useDefaultOrientation) break; + f->flip.set(i, orientation[i] != targetOrientation[i]); + } + } + + if(useDefaultOrientation) { + constexpr auto defaultOrientation = "rdi"_s; + constexpr Containers::StringView defaultDirections[3]{ + "right"_s, "down"_s, "forward"_s + }; + + const BoolVector3 flip = Math::notEqual( + Math::Vector3::from(defaultOrientation.data()), + Math::Vector3::from(targetOrientation.data())); + /* Leave non-existing dimensions unset, otherwise they affect the + result of any() and none() */ + for(UnsignedByte i = 0; i != f->numDimensions; ++i) + f->flip.set(i, flip[i]); + + Warning{} << "Trade::KtxImporter::openData(): missing or invalid " + "orientation, assuming" << ", "_s.join(Containers::arrayView(defaultDirections).prefix(f->numDimensions)); + } + } + + /* We can't reasonably perform axis flips on block-compressed data. + Emit a warning and pretend there is no flipping necessary. */ + if(f->pixelFormat.isCompressed && f->flip.any()) { + f->flip = BoolVector3{}; + Warning{} << "Trade::KtxImporter::openData(): block-compressed image " + "was encoded with non-default axis orientations, imported data " + "will have wrong orientation"; + } + + /** @todo KTX spec seems to really insist on rd for cube maps but the + wording is odd, I can't tell if they're saying it's mandatory or + not: https://github.khronos.org/KTX-Specification/#cubemapOrientation + The toktx tool from Khronos Texture Tools also forces rd for + cube maps, so we might want to do that in the converter as well. */ + + /* Incomplete cube maps are a 'feature' of KTX files. We just import them + as layers (which is how they're exposed to us). */ + if(numFaces != 6 && !keyValueEntries[KeyValueType::CubeMapIncomplete].value.empty()) { + Warning{} << "Trade::KtxImporter::openData(): image contains incomplete " + "cube map faces, importing faces as array layers"; + } + + /* Read swizzle information */ + if(!f->pixelFormat.isDepth) { + /* Remove trailing zero and anything after it */ + auto swizzle = Containers::StringView{keyValueEntries[KeyValueType::Swizzle].value}.partition('\0')[0]; + if(!swizzle.isEmpty()) { + /* We don't know the channel count of block-compressed formats so + comparing only the necessary prefix of the swizzle string can't + be done for them. The spec says to use 0 and 1 for missing + channels so in theory this is possible, but anyone who wants an + identity swizzle simply won't have an entry in the key/value + data in the first place. Reading the DFD for getting the channel + count is terrible overkill, so try our best for normal formats + and leave it at that. + */ + if(!f->pixelFormat.isCompressed) { + const std::size_t numChannels = f->pixelFormat.size / f->pixelFormat.typeSize; + swizzle = swizzle.prefix(Math::min(numChannels, swizzle.size())); + } + if(!"rgba"_s.hasPrefix(swizzle)) { + bool handled = false; + /* Special cases already supported for 8-bit Vulkan formats */ + if(!f->pixelFormat.isCompressed) { + if(swizzle == "bgr"_s) { + f->pixelFormat.swizzle ^= SwizzleType::BGR; + handled = true; + } else if(swizzle == "bgra"_s) { + f->pixelFormat.swizzle ^= SwizzleType::BGRA; + handled = true; + } + } + if(!handled) { + Error{} << "Trade::KtxImporter::openData(): unsupported channel " + "mapping" << swizzle; + return; + } + } + } + } + + if(flags() & ImporterFlag::Verbose) { + if(f->flip.any()) { + const Containers::StringView axes[3]{ + f->flip[0] ? "x"_s : ""_s, + f->flip[1] ? "y"_s : ""_s, + f->flip[2] ? "z"_s : ""_s, + }; + Debug{} << "Trade::KtxImporter::openData(): image will be flipped along" << + " and "_s.joinWithoutEmptyParts(axes); + } + + switch(f->pixelFormat.swizzle) { + case SwizzleType::BGR: + Debug{} << "Trade::KtxImporter::openData(): format requires conversion from BGR to RGB"; + break; + case SwizzleType::BGRA: + Debug{} << "Trade::KtxImporter::openData(): format requires conversion from BGRA to RGBA"; + break; + default: + break; + } + } + + /** @todo Read KTXanimData and expose frame time between images */ + + _f = std::move(f); +} + +template +ImageData KtxImporter::doImage(UnsignedInt id, UnsignedInt level) { + const File::LevelData& levelData = _f->imageData[id][level]; + const auto size = Math::Vector::pad(levelData.size); + Containers::Array data{NoInit, levelData.data.size()}; + + /* Block-compressed images don't have any flipping, swizzling or endian + swapping performed on them. Special-casing this mainly to avoid having + to calculate the block count for the strided array view. We already know + the entire data size. */ + if(_f->pixelFormat.isCompressed) { + CORRADE_INTERNAL_ASSERT(_f->flip.none()); + CORRADE_INTERNAL_ASSERT(_f->pixelFormat.swizzle == SwizzleType::None); + CORRADE_INTERNAL_ASSERT(_f->pixelFormat.typeSize == 1); + + Utility::copy(levelData.data, data); + return ImageData(_f->pixelFormat.compressed, size, std::move(data)); + } + + /* Uncompressed image */ + + /* Copy image data, flipping along axes if necessary. Assuming src is + tightly packed, stride gets calculated implicitly. */ + Containers::StridedArrayView4D src{levelData.data, { + std::size_t(levelData.size.z()), + std::size_t(levelData.size.y()), + std::size_t(levelData.size.x()), + _f->pixelFormat.size + }}; + Containers::StridedArrayView4D dst{data, src.size()}; + + if(_f->flip[2]) src = src.flipped<0>(); + if(_f->flip[1]) src = src.flipped<1>(); + if(_f->flip[0]) src = src.flipped<2>(); + + /* Without flipped dimensions this becomes a single memcpy */ + Utility::copy(src, dst); + + /* Swizzle BGR(A) if necessary */ + swizzlePixels(_f->pixelFormat.swizzle, _f->pixelFormat.typeSize, data); + + endianSwap(data, _f->pixelFormat.typeSize); + + /* Adjust pixel storage if row size is not four byte aligned */ + PixelStorage storage; + if((levelData.size.x()*_f->pixelFormat.size)%4 != 0) + storage.setAlignment(1); + + return ImageData{storage, _f->pixelFormat.uncompressed, size, std::move(data)}; +} + +UnsignedInt KtxImporter::doImage1DCount() const { return (_f->numDataDimensions == 1) ? _f->imageData.size() : 0; } + +UnsignedInt KtxImporter::doImage1DLevelCount(UnsignedInt id) { return _f->imageData[id].size(); } + +Containers::Optional KtxImporter::doImage1D(UnsignedInt id, UnsignedInt level) { + return doImage<1>(id, level); +} + +UnsignedInt KtxImporter::doImage2DCount() const { return (_f->numDataDimensions == 2) ? _f->imageData.size() : 0; } + +UnsignedInt KtxImporter::doImage2DLevelCount(UnsignedInt id) { return _f->imageData[id].size(); } + +Containers::Optional KtxImporter::doImage2D(UnsignedInt id, UnsignedInt level) { + return doImage<2>(id, level); +} + +UnsignedInt KtxImporter::doImage3DCount() const { return (_f->numDataDimensions == 3) ? _f->imageData.size() : 0; } + +UnsignedInt KtxImporter::doImage3DLevelCount(UnsignedInt id) { return _f->imageData[id].size(); } + +Containers::Optional KtxImporter::doImage3D(UnsignedInt id, const UnsignedInt level) { + return doImage<3>(id, level); +} + +UnsignedInt KtxImporter::doTextureCount() const { return _f->imageData.size(); } + +Containers::Optional KtxImporter::doTexture(UnsignedInt id) { + return TextureData{_f->type, SamplerFilter::Linear, SamplerFilter::Linear, + SamplerMipmap::Linear, SamplerWrapping::Repeat, id}; +} + +}} + +CORRADE_PLUGIN_REGISTER(KtxImporter, Magnum::Trade::KtxImporter, + "cz.mosra.magnum.Trade.AbstractImporter/0.3.3") diff --git a/src/MagnumPlugins/KtxImporter/KtxImporter.h b/src/MagnumPlugins/KtxImporter/KtxImporter.h new file mode 100644 index 000000000..e724b3410 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/KtxImporter.h @@ -0,0 +1,197 @@ +#ifndef Magnum_Trade_KtxImporter_h +#define Magnum_Trade_KtxImporter_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::Trade::KtxImporter + * @m_since_latest_{plugins} + */ + +#include + +#include "MagnumPlugins/KtxImporter/configure.h" + +#ifndef DOXYGEN_GENERATING_OUTPUT +#ifndef MAGNUM_KTXIMPORTER_BUILD_STATIC + #ifdef KtxImporter_EXPORTS + #define MAGNUM_KTXIMPORTER_EXPORT CORRADE_VISIBILITY_EXPORT + #else + #define MAGNUM_KTXIMPORTER_EXPORT CORRADE_VISIBILITY_IMPORT + #endif +#else + #define MAGNUM_KTXIMPORTER_EXPORT CORRADE_VISIBILITY_STATIC +#endif +#define MAGNUM_KTXIMPORTER_LOCAL CORRADE_VISIBILITY_LOCAL +#else +#define MAGNUM_KTXIMPORTER_EXPORT +#define MAGNUM_KTXIMPORTER_LOCAL +#endif + +namespace Magnum { namespace Trade { + +/** +@brief KTX2 image importer plugin +@m_since_latest_{plugins} + +Supports Khronos Texture 2.0 images (`*.ktx2`). + +@section Trade-KtxImporter-usage Usage + +This plugin depends on the @ref Trade library and is built if +`WITH_KTXIMPORTER` is enabled when building Magnum Plugins. To use as a dynamic +plugin, load @cpp "KtxImporter" @ce via @ref Corrade::PluginManager::Manager. + +Additionally, if you're using Magnum as a CMake subproject, bundle the +[magnum-plugins repository](https://github.com/mosra/magnum-plugins) and do the +following: + +@code{.cmake} +set(WITH_KTXIMPORTER ON CACHE BOOL "" FORCE) +add_subdirectory(magnum-plugins EXCLUDE_FROM_ALL) + +# So the dynamically loaded plugin gets built implicitly +add_dependencies(your-app MagnumPlugins::KtxImporter) +@endcode + +To use as a static plugin or as a dependency of another plugin with CMake, put +[FindMagnumPlugins.cmake](https://github.com/mosra/magnum-plugins/blob/master/modules/FindMagnumPlugins.cmake) +into your `modules/` directory, request the `KtxImporter` component of the +`MagnumPlugins` package in CMake and link to the `MagnumPlugins::KtxImporter` +target: + +@code{.cmake} +find_package(MagnumPlugins REQUIRED KtxImporter) + +# ... +target_link_libraries(your-app PRIVATE MagnumPlugins::KtxImporter) +@endcode + +See @ref building-plugins, @ref cmake-plugins, @ref plugins and +@ref file-formats for more information. + +@section Trade-KtxImporter-behavior Behavior and limitations + +Imports images in the following formats: + +- KTX2 with all uncompressed Vulkan formats that have an equivalent in + @ref PixelFormat, with component swizzling as necessary +- KTX2 with most compressed Vulkan formats that have an equivalent in + @ref CompressedPixelFormat. None of the 3D ASTC formats are supported. + +With compressed pixel formats, the image will not be flipped if the Y- or Z-axis +orientation doesn't match the output orientation. The nontrivial amount of work +involved with flipping block-compressed data makes this unfeasible. The import +will succeed but a warning will be emitted. + +The importer recognizes @ref ImporterFlag::Verbose, printing additional info +when the flag is enabled. + +@subsection Trade-KtxImporter-behavior-types Image types + +All image types supported by KTX2 are imported, including 1D, 2D, cube maps, +and 3D images. They can, in turn, all have multiple array layers as well as +multiple mip levels. The image type can be determined from @ref texture() and +@ref TextureData::type(). + +For layered images and (layered) cube maps, the array layers and faces are +exposed as an additional image dimension. 1D array textures import +@ref ImageData2D with n y-slices, (layered) 2D textures and cube maps import +@ref ImageData3D with 6*n z-slices. 3D array textures behave differently: +because there is no `ImageData4D`, each layer is imported as a separate +@ref ImageData3D, with @ref image3DCount() determining the number of layers. + +@subsection Trade-KtxImporter-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. + +@subsection Trade-KtxImporter-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. + +Incomplete cube maps (determined by the `KTXcubemapIncomplete` metadata entry) +are imported as a 2D array image, but information about which faces it contains +can't be imported. + +@subsection Trade-KtxImporter-behavior-supercompression Supercompression + +Importing files with [supercompression](https://github.khronos.org/KTX-Specification/#supercompressionSchemes) +is not supported. + +@subsection Trade-KtxImporter-behavior-swizzle Swizzle support + +Explicit swizzling via the KTXswizzle header entry supports BGR and BGRA. Any +other non-identity channel remapping is unsupported and results in an error. + +For reasons similar to the restriction on axis-flips, compressed formats don't +support any swizzling, and the import fails if an image with a compressed +format contains a swizzle that isn't RGBA. +*/ +class MAGNUM_KTXIMPORTER_EXPORT KtxImporter: public AbstractImporter { + public: + /** @brief Plugin manager constructor */ + explicit KtxImporter(PluginManager::AbstractManager& manager, const std::string& plugin); + + ~KtxImporter(); + + private: + MAGNUM_KTXIMPORTER_LOCAL ImporterFeatures doFeatures() const override; + MAGNUM_KTXIMPORTER_LOCAL bool doIsOpened() const override; + MAGNUM_KTXIMPORTER_LOCAL void doClose() override; + MAGNUM_KTXIMPORTER_LOCAL void doOpenData(Containers::ArrayView data) override; + + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doImage1DCount() const override; + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doImage1DLevelCount(UnsignedInt id) override; + MAGNUM_KTXIMPORTER_LOCAL Containers::Optional doImage1D(UnsignedInt id, UnsignedInt level) override; + + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doImage2DCount() const override; + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doImage2DLevelCount(UnsignedInt id) override; + MAGNUM_KTXIMPORTER_LOCAL Containers::Optional doImage2D(UnsignedInt id, UnsignedInt level) override; + + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doImage3DCount() const override; + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doImage3DLevelCount(UnsignedInt id) override; + MAGNUM_KTXIMPORTER_LOCAL Containers::Optional doImage3D(UnsignedInt id, UnsignedInt level) override; + + MAGNUM_KTXIMPORTER_LOCAL UnsignedInt doTextureCount() const override; + MAGNUM_KTXIMPORTER_LOCAL Containers::Optional doTexture(UnsignedInt id) override; + + private: + struct File; + Containers::Pointer _f; + + template + ImageData doImage(UnsignedInt id, UnsignedInt level); +}; + +}} + +#endif diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.bin b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.bin new file mode 100644 index 000000000..fd9c936b6 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.ktx2 b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.ktx2 new file mode 100644 index 000000000..a5e4539f9 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-bc1.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.bin b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.bin new file mode 100644 index 000000000..81368ffd9 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.ktx2 b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.ktx2 new file mode 100644 index 000000000..d70242799 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-etc2.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip0.bin b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip0.bin new file mode 100644 index 000000000..3cb42ca76 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip0.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip1.bin b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip1.bin new file mode 100644 index 000000000..289ffb850 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip1.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip2.bin b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip2.bin new file mode 100644 index 000000000..459e5ba67 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps-mip2.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps.ktx2 new file mode 100644 index 000000000..156400185 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-compressed-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-layers.ktx2 b/src/MagnumPlugins/KtxImporter/Test/1d-layers.ktx2 new file mode 100644 index 000000000..62c052035 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-layers.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/1d-mipmaps.ktx2 new file mode 100644 index 000000000..7bafa83ac Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/1d.ktx2 b/src/MagnumPlugins/KtxImporter/Test/1d.ktx2 new file mode 100644 index 000000000..70860ede7 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/1d.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.bin new file mode 100644 index 000000000..b6a953034 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.bin @@ -0,0 +1 @@ +98!s \ No newline at end of file diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.ktx2 new file mode 100644 index 000000000..570c25be1 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-astc.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.bin new file mode 100644 index 000000000..0500e401b Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.ktx2 new file mode 100644 index 000000000..3c2906a09 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc1.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.bin new file mode 100644 index 000000000..4a5bc65bc Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.ktx2 new file mode 100644 index 000000000..7542f590c Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-bc3.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.bin new file mode 100644 index 000000000..f1db6931d Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.ktx2 new file mode 100644 index 000000000..d91afadab Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-etc2.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-layers.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-layers.bin new file mode 100644 index 000000000..74f73bbc9 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-layers.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-layers.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-layers.ktx2 new file mode 100644 index 000000000..a939a87e0 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-layers.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip0.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip0.bin new file mode 100644 index 000000000..584253827 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip0.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip1.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip1.bin new file mode 100644 index 000000000..39d63a46c Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip1.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip2.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip2.bin new file mode 100644 index 000000000..6bd931695 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip2.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip3.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip3.bin new file mode 100644 index 000000000..459e5ba67 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps-mip3.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps.ktx2 new file mode 100644 index 000000000..345513a52 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.bin b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.bin new file mode 100644 index 000000000..1537766ad Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.ktx2 new file mode 100644 index 000000000..5b871d207 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-compressed-pvrtc.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-d16.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-d16.ktx2 new file mode 100644 index 000000000..a257d8e18 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-d16.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-d24s8.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-d24s8.ktx2 new file mode 100644 index 000000000..99de518c2 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-d24s8.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-d32fs8.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-d32fs8.ktx2 new file mode 100644 index 000000000..c8302c699 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-d32fs8.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-layers.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-layers.ktx2 new file mode 100644 index 000000000..66c696e7c Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-layers.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-and-layers.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-and-layers.ktx2 new file mode 100644 index 000000000..c5dbd3211 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-and-layers.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-incomplete.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-incomplete.ktx2 new file mode 100644 index 000000000..547759458 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps-incomplete.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps.ktx2 new file mode 100644 index 000000000..84d8c866c Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-rgb.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-rgb.ktx2 new file mode 100644 index 000000000..417fd5e1c Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-rgb.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-rgb32.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-rgb32.ktx2 new file mode 100644 index 000000000..056c4f425 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-rgb32.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-rgba.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-rgba.ktx2 new file mode 100644 index 000000000..ccd64de42 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-rgba.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-rgbf32.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-rgbf32.ktx2 new file mode 100644 index 000000000..e7e3f3e6d Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-rgbf32.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/2d-s8.ktx2 b/src/MagnumPlugins/KtxImporter/Test/2d-s8.ktx2 new file mode 100644 index 000000000..7d2b5820e Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/2d-s8.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip0.bin b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip0.bin new file mode 100644 index 000000000..c221dfab1 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip0.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip1.bin b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip1.bin new file mode 100644 index 000000000..5f465dcdb Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip1.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip2.bin b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip2.bin new file mode 100644 index 000000000..6bd931695 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip2.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip3.bin b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip3.bin new file mode 100644 index 000000000..459e5ba67 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps-mip3.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps.ktx2 new file mode 100644 index 000000000..a7082ad30 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed.bin b/src/MagnumPlugins/KtxImporter/Test/3d-compressed.bin new file mode 100644 index 000000000..8ff1eed30 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed.bin differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-compressed.ktx2 b/src/MagnumPlugins/KtxImporter/Test/3d-compressed.ktx2 new file mode 100644 index 000000000..0af0287aa Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-compressed.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-layers.ktx2 b/src/MagnumPlugins/KtxImporter/Test/3d-layers.ktx2 new file mode 100644 index 000000000..c4087a410 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-layers.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/3d-mipmaps.ktx2 new file mode 100644 index 000000000..9b9b8e773 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/3d.ktx2 b/src/MagnumPlugins/KtxImporter/Test/3d.ktx2 new file mode 100644 index 000000000..39c10268d Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/3d.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/CMakeLists.txt b/src/MagnumPlugins/KtxImporter/Test/CMakeLists.txt new file mode 100644 index 000000000..1d67de4fd --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/Test/CMakeLists.txt @@ -0,0 +1,129 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# 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"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) + set(KTXIMPORTER_TEST_DIR ".") +else() + set(KTXIMPORTER_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + +# CMake before 3.8 has broken $ expressions for iOS (see +# https://gitlab.kitware.com/cmake/cmake/merge_requests/404) and since Corrade +# doesn't support dynamic plugins on iOS, this sorta works around that. Should +# be revisited when updating Travis to newer Xcode (xcode7.3 has CMake 3.6). +if(NOT MAGNUM_KTXIMPORTER_BUILD_STATIC) + set(KTXIMPORTER_PLUGIN_FILENAME $) +endif() + +# First replace ${} variables, then $<> generator expressions +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) +file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/configure.h + INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) + +corrade_add_test(KtxImporterTest KtxImporterTest.cpp + LIBRARIES Magnum::Trade + FILES + 1d-compressed-bc1.bin + 1d-compressed-bc1.ktx2 + 1d-compressed-etc2.bin + 1d-compressed-etc2.ktx2 + 1d-compressed-mipmaps-mip0.bin + 1d-compressed-mipmaps-mip1.bin + 1d-compressed-mipmaps-mip2.bin + 1d-compressed-mipmaps.ktx2 + 1d-layers.ktx2 + 1d-mipmaps.ktx2 + 1d.ktx2 + 2d-compressed-astc.bin + 2d-compressed-astc.ktx2 + 2d-compressed-bc1.bin + 2d-compressed-bc1.ktx2 + 2d-compressed-bc3.bin + 2d-compressed-bc3.ktx2 + 2d-compressed-etc2.bin + 2d-compressed-etc2.ktx2 + 2d-compressed-layers.bin + 2d-compressed-layers.ktx2 + 2d-compressed-mipmaps-mip0.bin + 2d-compressed-mipmaps-mip1.bin + 2d-compressed-mipmaps-mip2.bin + 2d-compressed-mipmaps-mip3.bin + 2d-compressed-mipmaps.ktx2 + 2d-compressed-pvrtc.bin + 2d-compressed-pvrtc.ktx2 + 2d-d16.ktx2 + 2d-d24s8.ktx2 + 2d-d32fs8.ktx2 + 2d-layers.ktx2 + 2d-mipmaps-and-layers.ktx2 + 2d-mipmaps-incomplete.ktx2 + 2d-mipmaps.ktx2 + 2d-rgb.ktx2 + 2d-rgb32.ktx2 + 2d-rgba.ktx2 + 2d-rgbf32.ktx2 + 2d-s8.ktx2 + 3d-compressed.bin + 3d-compressed.ktx2 + 3d-compressed-mipmaps-mip0.bin + 3d-compressed-mipmaps-mip1.bin + 3d-compressed-mipmaps-mip2.bin + 3d-compressed-mipmaps-mip3.bin + 3d-compressed-mipmaps.ktx2 + 3d-layers.ktx2 + 3d-mipmaps.ktx2 + 3d.ktx2 + bgr-swizzle-bgr-16bit.ktx2 + bgr-swizzle-bgr.ktx2 + bgr.ktx2 + bgra-swizzle-bgra.ktx2 + bgra.ktx2 + cubemap-layers.ktx2 + cubemap-mipmaps.ktx2 + cubemap.ktx2 + swizzle-bgr.ktx2 + swizzle-bgra.ktx2 + swizzle-identity.ktx2 + swizzle-unsupported.ktx2 + version1.ktx) +target_include_directories(KtxImporterTest PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/$ + ${PROJECT_SOURCE_DIR}/src) +if(MAGNUM_KTXIMPORTER_BUILD_STATIC) + target_link_libraries(KtxImporterTest PRIVATE KtxImporter) +else() + # So the plugins get properly built when building the test + add_dependencies(KtxImporterTest KtxImporter) +endif() +set_target_properties(KtxImporterTest PROPERTIES FOLDER "MagnumPlugins/KtxImporter/Test") +if(CORRADE_BUILD_STATIC AND NOT MAGNUM_KTXIMPORTER_BUILD_STATIC) + # CMake < 3.4 does this implicitly, but 3.4+ not anymore (see CMP0065). + # That's generally okay, *except if* the build is static, the executable + # uses a plugin manager and needs to share globals with the plugins (such + # as output redirection and so on). + set_target_properties(KtxImporterTest PROPERTIES ENABLE_EXPORTS ON) +endif() diff --git a/src/MagnumPlugins/KtxImporter/Test/KtxImporterTest.cpp b/src/MagnumPlugins/KtxImporter/Test/KtxImporterTest.cpp new file mode 100644 index 000000000..e6fde5b79 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/Test/KtxImporterTest.cpp @@ -0,0 +1,1791 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MagnumPlugins/KtxImporter/KtxHeader.h" + +#include "configure.h" + +namespace Magnum { namespace Trade { namespace Test { namespace { + +struct KtxImporterTest: TestSuite::Tester { + explicit KtxImporterTest(); + + void openShort(); + + void invalid(); + void invalidVersion(); + void invalidFormat(); + + void texture(); + + void imageRgba(); + void imageRgb32U(); + void imageRgb32F(); + void imageDepthStencil(); + + void image1D(); + void image1DMipmaps(); + void image1DLayers(); + void image1DCompressed(); + void image1DCompressedMipmaps(); + + void image2D(); + void image2DMipmaps(); + void image2DMipmapsIncomplete(); + void image2DLayers(); + void image2DMipmapsAndLayers(); + void image2DCompressed(); + void image2DCompressedMipmaps(); + void image2DCompressedLayers(); + + void imageCubeMapIncomplete(); + void imageCubeMap(); + void imageCubeMapLayers(); + void imageCubeMapMipmaps(); + + void image3D(); + void image3DMipmaps(); + void image3DLayers(); + void image3DCompressed(); + void image3DCompressedMipmaps(); + + void keyValueDataEmpty(); + void keyValueDataInvalid(); + void keyValueDataInvalidIgnored(); + + void orientationInvalid(); + void orientationFlip(); + void orientationFlipCompressed(); + + void swizzle(); + void swizzleMultipleBytes(); + void swizzleIdentity(); + void swizzleUnsupported(); + void swizzleCompressed(); + + void openTwice(); + void importTwice(); + + /* Explicitly forbid system-wide plugin dependencies */ + PluginManager::Manager _manager{"nonexistent"}; +}; + +using namespace Math::Literals; + +const Color3ub PatternRgb1DData[3][4]{ + /* pattern-1d.png */ + {0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x007f7f_rgb}, + /* pattern-1d.png */ + {0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x007f7f_rgb}, + /* black-1d.png */ + {0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb} +}; + +/* Origin bottom-left */ +const Color3ub PatternRgbData[3][3][4]{ + /* pattern.png */ + {{0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x00ff00_rgb}, + {0xffffff_rgb, 0xff0000_rgb, 0x000000_rgb, 0x00ff00_rgb}, + {0x0000ff_rgb, 0x00ff00_rgb, 0x7f007f_rgb, 0x7f007f_rgb}}, + /* pattern.png */ + {{0xff0000_rgb, 0xffffff_rgb, 0x000000_rgb, 0x00ff00_rgb}, + {0xffffff_rgb, 0xff0000_rgb, 0x000000_rgb, 0x00ff00_rgb}, + {0x0000ff_rgb, 0x00ff00_rgb, 0x7f007f_rgb, 0x7f007f_rgb}}, + /* black.png */ + {{0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb}, + {0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb}, + {0x000000_rgb, 0x000000_rgb, 0x000000_rgb, 0x000000_rgb}} +}; + +const Color4ub PatternRgba2DData[3][4]{ + {PatternRgbData[0][0][0], PatternRgbData[0][0][1], PatternRgbData[0][0][2], PatternRgbData[0][0][3]}, + {PatternRgbData[0][1][0], PatternRgbData[0][1][1], PatternRgbData[0][1][2], PatternRgbData[0][1][3]}, + {PatternRgbData[0][2][0], PatternRgbData[0][2][1], PatternRgbData[0][2][2], PatternRgbData[0][2][3]} +}; + +constexpr UnsignedByte PatternStencil8UIData[4*3]{ + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12 +}; + +constexpr UnsignedShort PatternDepth16UnormData[4*3]{ + 0xff01, 0xff02, 0xff03, 0xff04, + 0xff05, 0xff06, 0xff07, 0xff08, + 0xff09, 0xff10, 0xff11, 0xff12 +}; + +constexpr UnsignedInt PatternDepth24UnormStencil8UIData[4*3]{ + 0xffffff01, 0xffffff02, 0xffffff03, 0xffffff04, + 0xffffff05, 0xffffff06, 0xffffff07, 0xffffff08, + 0xffffff09, 0xffffff10, 0xffffff11, 0xffffff12 +}; + +constexpr UnsignedLong HalfL = 0x7f7f7f7f7f7f7f7f; +constexpr UnsignedLong FullL = 0xffffffffffffffff; +constexpr UnsignedLong PatternDepth32FStencil8UIData[4*3]{ + 0, 0, 0, HalfL, + 0, FullL, FullL, HalfL, + 0, FullL, 0, FullL +}; + +const struct { + const char* name; + const std::size_t length; + const char* message; +} ShortData[]{ + {"identifier", sizeof(Implementation::KtxHeader::identifier) - 1, + "file too short, expected 80 bytes for the header but got only 11"}, + {"header", sizeof(Implementation::KtxHeader) - 1, + "file too short, expected 80 bytes for the header but got only 79"}, + {"level index", sizeof(Implementation::KtxHeader) + sizeof(Implementation::KtxLevel) - 1, + "file too short, expected 104 bytes for level index but got only 103"}, + {"key/value data", sizeof(Implementation::KtxHeader) + sizeof(Implementation::KtxLevel) + sizeof(UnsignedInt) + sizeof(Implementation::KdfBasicBlockHeader) + 3*sizeof(Implementation::KdfBasicBlockSample), + "file too short, expected 252 bytes for key/value data but got only 180"}, + {"level data", 287, + "file too short, expected 288 bytes for level data but got only 287"} +}; + +constexpr UnsignedByte VK_FORMAT_D32_SFLOAT = 126; + +const struct { + const char* name; + const char* file; + const std::size_t offset; + const char value; + const char* message; +} InvalidData[]{ + {"signature", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, identifier) + sizeof(Implementation::KtxHeader::identifier) - 1, 0, + "wrong file signature"}, + {"type size", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, typeSize), 7, + "unsupported type size 7"}, + {"image size x", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, imageSize), 0, + "invalid image size, width is 0"}, + {"image size y", "3d.ktx2", + offsetof(Implementation::KtxHeader, imageSize) + sizeof(UnsignedInt), 0, + "invalid image size, depth is 3 but height is 0"}, + {"face count", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, faceCount), 3, + "expected either 1 or 6 faces for cube maps but got 3"}, + {"cube not square", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, faceCount), 6, + "cube map dimensions must be 2D and square, but got Vector(4, 3, 0)"}, + {"cube 3d", "3d.ktx2", + offsetof(Implementation::KtxHeader, faceCount), 6, + "cube map dimensions must be 2D and square, but got Vector(4, 3, 3)"}, + {"level count", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, levelCount), 7, + "expected at most 3 mip levels but got 7"}, + {"custom format", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, vkFormat), 0, + "custom formats are not supported"}, + {"compressed type size", "2d-compressed-etc2.ktx2", + offsetof(Implementation::KtxHeader, typeSize), 4, + "invalid type size for compressed format, expected 1 but got 4"}, + {"supercompression", "2d-rgb.ktx2", + offsetof(Implementation::KtxHeader, supercompressionScheme), 1, + "supercompression is currently not supported"}, + {"3d depth", "3d.ktx2", + offsetof(Implementation::KtxHeader, vkFormat), VK_FORMAT_D32_SFLOAT, + "3D images can't have depth/stencil format"}, + {"level data too short", "2d-rgb.ktx2", + sizeof(Implementation::KtxHeader) + offsetof(Implementation::KtxLevel, byteLength), 1, + "level data too short, expected at least 36 bytes but got 1"}, + {"3D layered level data too short", "3d-layers.ktx2", + sizeof(Implementation::KtxHeader) + offsetof(Implementation::KtxLevel, byteLength), 108, + "level data too short, expected at least 216 bytes but got 108"} +}; + +const struct { + const char* name; + const char* file; + const TextureType type; +} TextureData[]{ + {"1D", "1d.ktx2", TextureType::Texture1D}, + {"1D array", "1d-layers.ktx2", TextureType::Texture1DArray}, + {"2D", "2d-rgb.ktx2", TextureType::Texture2D}, + {"2D array", "2d-layers.ktx2", TextureType::Texture2DArray}, + {"cube map", "cubemap.ktx2", TextureType::CubeMap}, + {"cube map array", "cubemap-layers.ktx2", TextureType::CubeMapArray}, + {"3D", "3d.ktx2", TextureType::Texture3D}, + {"3D array", "3d-layers.ktx2", TextureType::Texture3D} +}; + +const struct { + const char* name; + const char* file; + const PixelFormat format; + const Containers::ArrayView data; +} DepthStencilImageData[]{ + {"Stencil8UI", "2d-s8.ktx2", PixelFormat::Stencil8UI, + Containers::arrayCast(PatternStencil8UIData)}, + {"Depth16Unorm", "2d-d16.ktx2", PixelFormat::Depth16Unorm, + Containers::arrayCast(PatternDepth16UnormData)}, + {"Depth24UnormStencil8UI", "2d-d24s8.ktx2", PixelFormat::Depth24UnormStencil8UI, + Containers::arrayCast(PatternDepth24UnormStencil8UIData)}, + {"Depth32FStencil8UI", "2d-d32fs8.ktx2", PixelFormat::Depth32FStencil8UI, + Containers::arrayCast(PatternDepth32FStencil8UIData)} +}; + +const struct { + const char* name; + const char* file; + const CompressedPixelFormat format; + const Math::Vector<1, Int> size; +} CompressedImage1DData[]{ + {"BC1", "1d-compressed-bc1.ktx2", CompressedPixelFormat::Bc1RGBASrgb, {4}}, + {"ETC2", "1d-compressed-etc2.ktx2", CompressedPixelFormat::Etc2RGB8Srgb, {7}} +}; + +const struct { + const char* name; + const char* file; + const CompressedPixelFormat format; + const Vector2i size; +} CompressedImage2DData[]{ + {"PVRTC", "2d-compressed-pvrtc.ktx2", CompressedPixelFormat::PvrtcRGBA4bppSrgb, {8, 8}}, + {"BC1", "2d-compressed-bc1.ktx2", CompressedPixelFormat::Bc1RGBASrgb, {8, 8}}, + {"BC3", "2d-compressed-bc3.ktx2", CompressedPixelFormat::Bc3RGBASrgb, {8, 8}}, + {"ETC2", "2d-compressed-etc2.ktx2", CompressedPixelFormat::Etc2RGB8Srgb, {9, 10}}, + {"ASTC", "2d-compressed-astc.ktx2", CompressedPixelFormat::Astc12x10RGBASrgb, {9, 10}} +}; + +using namespace Containers::Literals; + +const struct { + const char* name; + const Containers::StringView data; + const char* message; +} InvalidKeyValueData[]{ + /* Entry has length 0, followed by a valid entry (with an empty value, that's allowed) */ + {"zero length", "\x00\x00\x00\x00\x02\x00\x00\x00k\x00\x00\x00"_s, "invalid key/value entry, skipping"}, + /* Key has length 0, followed by padding + a valid entry */ + {"empty key", "\x02\x00\x00\x00\x00v\x00\x00\x02\x00\x00\x00k\x00\x00\x00"_s, "invalid key/value entry, skipping"}, + /* Duplicate key check only happens for specific keys used later */ + {"duplicate key", "\x10\x00\x00\x00KTXswizzle\x00rgba\x00\x10\x00\x00\x00KTXswizzle\x00rgba\x00"_s, "key KTXswizzle already set, skipping"}, +}; + +const struct { + const char* name; + const Containers::StringView data; +} IgnoredInvalidKeyValueData[]{ + /* Length extends beyond key/value data */ + {"length out of bounds", "\xff\x00\x00\x00k\x00\x00\x00"_s}, + /* Importer shouldn't care about order of keys */ + {"unsorted keys", "\x02\x00\x00\x00b\x00\x00\x00\x02\x00\x00\x00a\x00\x00\x00"_s} +}; + +const struct { + const char* name; + const char* file; + const UnsignedInt dimensions; + const Containers::StringView orientation; +} InvalidOrientationData[]{ + {"empty", "1d.ktx2", 1, ""_s}, + {"short", "2d-rgb.ktx2", 2, "r"_s}, + {"invalid x", "2d-rgb.ktx2", 2, "xd"_s}, + {"invalid y", "2d-rgb.ktx2", 2, "rx"_s}, + {"invalid z", "3d.ktx2", 3, "rux"_s}, +}; + +const struct { + const char* name; + const char* file; + const Vector3i size; + const PixelFormat format; + const Containers::ArrayView data; + const Vector3ub flipped; +} FlipData[]{ + /* Don't test everything, just a few common and interesting orientations */ + {"l", "1d.ktx2", {4, 0, 0}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgb1DData[0]), {true, false, false}}, + {"r", "1d.ktx2", {4, 0, 0}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgb1DData[0]), {false, false, false}}, + /* Value of flipped is relative to the orientation on disk. Files are rd[i], + the ground truth data expects a flip to ru[o]. */ + {"lu", "2d-rgb.ktx2", {4, 3, 0}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgbData[0]), {true, true, false}}, + {"rd", "2d-rgb.ktx2", {4, 3, 0}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgbData[0]), {false, false, false}}, + {"luo", "3d.ktx2", {4, 3, 3}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgbData), {true, true, true}}, + {"rdo", "3d.ktx2", {4, 3, 3}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgbData), {false, false, true}}, + {"rdi", "3d.ktx2", {4, 3, 3}, PixelFormat::RGB8Srgb, + Containers::arrayCast(PatternRgbData), {false, false, false}} +}; + +const struct { + const char* name; + const char* file; + const PixelFormat format; + const Implementation::VkFormat vkFormat; + const char* message; + const Containers::ArrayView data; +} SwizzleData[]{ + {"BGR8 header", "bgr-swizzle-bgr.ktx2", + PixelFormat::RGB8Srgb, Implementation::VK_FORMAT_UNDEFINED, + "format requires conversion from BGR to RGB", Containers::arrayCast(PatternRgbData[0])}, + {"BGRA8 header", "bgra-swizzle-bgra.ktx2", + PixelFormat::RGBA8Srgb, Implementation::VK_FORMAT_UNDEFINED, + "format requires conversion from BGRA to RGBA", Containers::arrayCast(PatternRgba2DData)}, + {"BGR8 format", "bgr.ktx2", + PixelFormat::RGB8Srgb, Implementation::VK_FORMAT_B8G8R8_SRGB, + "format requires conversion from BGR to RGB", Containers::arrayCast(PatternRgbData[0])}, + {"BGRA8 format", "bgra.ktx2", + PixelFormat::RGBA8Srgb, Implementation::VK_FORMAT_B8G8R8A8_SRGB, + "format requires conversion from BGRA to RGBA", Containers::arrayCast(PatternRgba2DData)}, + {"BGR8 format+header cancel", "swizzle-bgr.ktx2", + PixelFormat::RGB8Srgb, Implementation::VK_FORMAT_B8G8R8_SRGB, + nullptr, Containers::arrayCast(PatternRgbData[0])}, + {"BGRA8 format+header cancel", "swizzle-bgra.ktx2", + PixelFormat::RGBA8Srgb, Implementation::VK_FORMAT_B8G8R8A8_SRGB, + nullptr, Containers::arrayCast(PatternRgba2DData)}, + {"depth header ignored", "swizzle-bgra.ktx2", + PixelFormat::Depth32F, VK_FORMAT_D32_SFLOAT, + nullptr, Containers::arrayCast(PatternRgba2DData)} +}; + +Containers::Array createKeyValueData(Containers::StringView key, Containers::ArrayView value, bool terminatingZero = false) { + UnsignedInt size = key.size() + 1 + value.size() + UnsignedInt(terminatingZero); + size = (size + 3)/4*4; + Containers::Array keyValueData{ValueInit, sizeof(UnsignedInt) + size}; + + std::size_t offset = 0; + *reinterpret_cast(keyValueData.data()) = Utility::Endianness::littleEndian(size); + offset += sizeof(size); + Utility::copy(key, keyValueData.suffix(offset).prefix(key.size())); + offset += key.size() + 1; + Utility::copy(value, keyValueData.suffix(offset).prefix(value.size())); + + return keyValueData; +} + +Containers::Array createKeyValueData(Containers::StringView key, Containers::StringView value) { + return createKeyValueData(key, value, true); +} + +void patchKeyValueData(Containers::ArrayView keyValueData, Containers::ArrayView fileData) { + CORRADE_INTERNAL_ASSERT(fileData.size() >= sizeof(Implementation::KtxHeader)); + Implementation::KtxHeader& header = *reinterpret_cast(fileData.data()); + Utility::Endianness::littleEndianInPlace(header.kvdByteOffset, header.kvdByteLength); + + CORRADE_INTERNAL_ASSERT(header.kvdByteOffset + keyValueData.size() <= fileData.size()); + CORRADE_INTERNAL_ASSERT(header.kvdByteLength >= keyValueData.size()); + header.kvdByteLength = keyValueData.size(); + Utility::copy(keyValueData, fileData.suffix(header.kvdByteOffset).prefix(header.kvdByteLength)); + + Utility::Endianness::littleEndianInPlace(header.kvdByteOffset, header.kvdByteLength); +} + +KtxImporterTest::KtxImporterTest() { + addInstancedTests({&KtxImporterTest::openShort}, + Containers::arraySize(ShortData)); + + addInstancedTests({&KtxImporterTest::invalid}, + Containers::arraySize(InvalidData)); + + addTests({&KtxImporterTest::invalidVersion, + &KtxImporterTest::invalidFormat}); + + addInstancedTests({&KtxImporterTest::texture}, + Containers::arraySize(TextureData)); + + addTests({&KtxImporterTest::imageRgba, + &KtxImporterTest::imageRgb32U, + &KtxImporterTest::imageRgb32F}); + + addInstancedTests({&KtxImporterTest::imageDepthStencil}, + Containers::arraySize(DepthStencilImageData)); + + addTests({&KtxImporterTest::image1D, + &KtxImporterTest::image1DMipmaps, + &KtxImporterTest::image1DLayers}); + + addInstancedTests({&KtxImporterTest::image1DCompressed}, + Containers::arraySize(CompressedImage1DData)); + + addTests({&KtxImporterTest::image1DCompressedMipmaps, + + &KtxImporterTest::image2D, + &KtxImporterTest::image2DMipmaps, + &KtxImporterTest::image2DMipmapsIncomplete, + &KtxImporterTest::image2DLayers, + &KtxImporterTest::image2DMipmapsAndLayers}); + + addInstancedTests({&KtxImporterTest::image2DCompressed}, + Containers::arraySize(CompressedImage2DData)); + + addTests({&KtxImporterTest::image2DCompressedMipmaps, + &KtxImporterTest::image2DCompressedLayers, + + &KtxImporterTest::imageCubeMapIncomplete, + &KtxImporterTest::imageCubeMap, + &KtxImporterTest::imageCubeMapLayers, + &KtxImporterTest::imageCubeMapMipmaps, + + &KtxImporterTest::image3D, + &KtxImporterTest::image3DMipmaps, + &KtxImporterTest::image3DLayers, + &KtxImporterTest::image3DCompressed, + &KtxImporterTest::image3DCompressedMipmaps, + + &KtxImporterTest::keyValueDataEmpty}); + + addInstancedTests({&KtxImporterTest::keyValueDataInvalid}, + Containers::arraySize(InvalidKeyValueData)); + + addInstancedTests({&KtxImporterTest::keyValueDataInvalidIgnored}, + Containers::arraySize(IgnoredInvalidKeyValueData)); + + addInstancedTests({&KtxImporterTest::orientationInvalid}, + Containers::arraySize(InvalidOrientationData)); + + addInstancedTests({&KtxImporterTest::orientationFlip}, + Containers::arraySize(FlipData)); + + addTests({&KtxImporterTest::orientationFlipCompressed}); + + addInstancedTests({&KtxImporterTest::swizzle}, + Containers::arraySize(SwizzleData)); + + addTests({&KtxImporterTest::swizzleMultipleBytes, + &KtxImporterTest::swizzleIdentity, + &KtxImporterTest::swizzleUnsupported, + &KtxImporterTest::swizzleCompressed, + + &KtxImporterTest::openTwice, + &KtxImporterTest::importTwice}); + + /* Load the plugin directly from the build tree. Otherwise it's static and + already loaded. */ + #ifdef KTXIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(KTXIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif +} + +void KtxImporterTest::openShort() { + auto&& data = ShortData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + const auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2")); + CORRADE_INTERNAL_ASSERT(data.length < fileData.size()); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openData(fileData.prefix(data.length))); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::KtxImporter::openData(): {}\n", data.message)); +} + +void KtxImporterTest::invalid() { + auto&& data = InvalidData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + CORRADE_INTERNAL_ASSERT(data.offset < fileData.size()); + + fileData[data.offset] = data.value; + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openData(fileData)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::KtxImporter::openData(): {}\n", data.message)); +} + +void KtxImporterTest::invalidVersion() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "version1.ktx"))); + CORRADE_COMPARE(out.str(), "Trade::KtxImporter::openData(): unsupported KTX version, expected 20 but got 11\n"); +} + +void KtxImporterTest::invalidFormat() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2")); + CORRADE_VERIFY(fileData.size() >= sizeof(Implementation::KtxHeader)); + + Implementation::KtxHeader& header = *reinterpret_cast(fileData.data()); + + /* Selected unsupported formats. Implementation::VkFormat only contains + swizzled 8-bit formats so we have to define our own. + Taken from magnum/src/MagnumExternal/Vulkan/flextVk.h + (commit 9d4a8b49943a084cff64550792bb2eba223e0e03) */ + enum VkFormat : UnsignedInt { + VK_FORMAT_R4G4_UNORM_PACK8 = 1, + VK_FORMAT_A1R5G5B5_UNORM_PACK16 = 8, + VK_FORMAT_R8_USCALED = 11, + VK_FORMAT_R16_SSCALED = 73, + VK_FORMAT_R64_UINT = 110, + VK_FORMAT_R64G64B64A64_SFLOAT = 121, + VK_FORMAT_G8B8G8R8_422_UNORM = 1000156000, + VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM = 1000156002, + VK_FORMAT_R10X6G10X6_UNORM_2PACK16 = 1000156008, + VK_FORMAT_G16B16G16R16_422_UNORM = 1000156027, + VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG = 1000054006 + }; + + constexpr Implementation::VkFormat formats[]{ + /* Not allowed by KTX. All of the unsupported formats happen to not be + supported by Magnum, either. */ + VK_FORMAT_R4G4_UNORM_PACK8, + VK_FORMAT_A1R5G5B5_UNORM_PACK16, + VK_FORMAT_R8_USCALED, + VK_FORMAT_R16_SSCALED, + VK_FORMAT_G8B8G8R8_422_UNORM, + VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM, + VK_FORMAT_R10X6G10X6_UNORM_2PACK16, + VK_FORMAT_G16B16G16R16_422_UNORM, + /* Not supported by Magnum */ + VK_FORMAT_R64_UINT, + VK_FORMAT_R64G64B64A64_SFLOAT, + VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG + }; + + for(UnsignedInt i = 0; i != Containers::arraySize(formats); ++i) { + CORRADE_ITERATION(i); + header.vkFormat = Utility::Endianness::littleEndian(formats[i]); + + std::ostringstream out; + Error redirectError{&out}; + CORRADE_VERIFY(!importer->openData(fileData)); + CORRADE_COMPARE(out.str(), Utility::formatString("Trade::KtxImporter::openData(): unsupported format {}\n", UnsignedInt(formats[i]))); + } +} + +void KtxImporterTest::texture() { + auto&& data = TextureData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file))); + + 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(), data.type); + } + + UnsignedInt dimensions; + switch(data.type) { + case TextureType::Texture1D: + dimensions = 1; + break; + case TextureType::Texture1DArray: + case TextureType::Texture2D: + dimensions = 2; + break; + case TextureType::Texture2DArray: + case TextureType::Texture3D: + case TextureType::CubeMap: + case TextureType::CubeMapArray: + dimensions = 3; + break; + default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + } + CORRADE_COMPARE(counts[dimensions - 1], total); +} + +void KtxImporterTest::imageRgba() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgba.ktx2"))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(PatternRgba2DData), TestSuite::Compare::Container); +} + +void KtxImporterTest::imageRgb32U() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb32.ktx2"))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB32UI); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + /* Output of PVRTexTool with format conversion. This is PatternRgbData[0], + but each byte extended to uint by just repeating the byte 4 times. */ + constexpr UnsignedInt Half = 0x7f7f7f7f; + constexpr Math::Color3 content[4*3]{ + {~0u, 0, 0}, {~0u, ~0u, ~0u}, { 0, 0, 0}, { 0, ~0u, 0}, + {~0u, ~0u, ~0u}, {~0u, 0, 0}, { 0, 0, 0}, { 0, ~0u, 0}, + { 0, 0, ~0u}, { 0, ~0u, 0}, {Half, 0, Half}, {Half, 0, Half} + }; + + CORRADE_COMPARE_AS(Containers::arrayCast>(image->data()), + Containers::arrayView(content), TestSuite::Compare::Container); +} + +void KtxImporterTest::imageRgb32F() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgbf32.ktx2"))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB32F); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + /* Output of PVRTexTool with format conversion. This is PatternRgbData[0], + but each byte mapped to the range 0.0 - 1.0. */ + constexpr Float Half = 127.0f/255.0f; + constexpr Math::Color3 content[4*3]{ + {1.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, + {1.0f, 1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, + {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, {Half, 0.0f, Half}, {Half, 0.0f, Half} + }; + + CORRADE_COMPARE_AS(Containers::arrayCast>(image->data()), + Containers::arrayView(content), TestSuite::Compare::Container); +} + +void KtxImporterTest::imageDepthStencil() { + auto&& data = DepthStencilImageData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), data.format); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), data.data, TestSuite::Compare::Container); +} + +void KtxImporterTest::image1D() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d.ktx2"))); + + CORRADE_COMPARE(importer->image1DCount(), 1); + CORRADE_COMPARE(importer->image1DLevelCount(0), 1); + + auto image = importer->image1D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Math::Vector<1, Int>{4})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(PatternRgb1DData[0]), TestSuite::Compare::Container); +} + +void KtxImporterTest::image1DMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-mipmaps.ktx2"))); + + const auto mip0 = Containers::arrayView(PatternRgb1DData[0]); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Color3ub mip2[1]{0x000000_rgb}; + const Containers::ArrayView mipViews[3]{mip0, mip1, mip2}; + + CORRADE_COMPARE(importer->image1DCount(), 1); + CORRADE_COMPARE(importer->image1DLevelCount(0), Containers::arraySize(mipViews)); + + Math::Vector<1, Int> mipSize{4}; + for(UnsignedInt i = 0; i != importer->image1DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image1D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + + const PixelStorage storage = image->storage(); + /* Alignment is 4 when row length is a multiple of 4 */ + const Int alignment = ((mipSize[0]*image->pixelSize())%4 == 0) ? 4 : 1; + CORRADE_COMPARE(storage.alignment(), alignment); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(mipViews[i]), TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image1DLayers() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-layers.ktx2"))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(PatternRgb1DData), TestSuite::Compare::Container); +} + +void KtxImporterTest::image1DCompressed() { + auto&& data = CompressedImage1DData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file))); + + CORRADE_COMPARE(importer->image1DCount(), 1); + CORRADE_COMPARE(importer->image1DLevelCount(0), 1); + + auto image = importer->image1D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), data.format); + CORRADE_COMPARE(image->size(), data.size); + + const CompressedPixelStorage storage = image->compressedStorage(); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + /* The compressed data is the output of PVRTexTool, nothing hand-crafted. + Use --save-diagnostic to extract them if they're missing or wrong. The + same files are re-used in the tests for KtxImageConverter as input data. */ + const Vector3i blockSize = compressedBlockSize(data.format); + const Vector3i blockCount = (Vector3i::pad(data.size, 1) + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(data.format)); + CORRADE_COMPARE_AS(std::string(image->data().data(), image->data().size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::Directory::splitExtension(data.file).first + ".bin"), + TestSuite::Compare::StringToFile); +} + +void KtxImporterTest::image1DCompressedMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "1d-compressed-mipmaps.ktx2"))); + + CORRADE_COMPARE(importer->image1DCount(), 1); + CORRADE_COMPARE(importer->image1DLevelCount(0), 3); + + Math::Vector<1, Int> mipSize{7}; + for(UnsignedInt i = 0; i != importer->image1DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image1D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + + const Vector3i blockSize = compressedBlockSize(image->compressedFormat()); + const Vector3i blockCount = (Vector3i::pad(mipSize, 1) + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(image->compressedFormat())); + /* This is suboptimal because when generating ground-truth data with + --save-diagnostic the test needs to be run 4 times to save all mips. + But hopefully this won't really be necessary. */ + CORRADE_COMPARE_AS(std::string(image->data().data(), image->data().size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::formatString("1d-compressed-mipmaps-mip{}.bin", i)), + TestSuite::Compare::StringToFile); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image2D() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2"))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(PatternRgbData[0]), TestSuite::Compare::Container); +} + +void KtxImporterTest::image2DMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-mipmaps.ktx2"))); + + /* Is there a nicer way to get a flat view for a multi-dimensional array? */ + const auto mip0 = Containers::arrayCast(PatternRgbData[0]); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Color3ub mip2[1]{0x000000_rgb}; + const Containers::ArrayView mipViews[3]{mip0, mip1, mip2}; + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), Containers::arraySize(mipViews)); + + Vector2i mipSize{4, 3}; + for(UnsignedInt i = 0; i != importer->image2DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image2D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + + const PixelStorage storage = image->storage(); + /* Alignment is 4 when row length is a multiple of 4 */ + const Int alignment = ((mipSize.x()*image->pixelSize())%4 == 0) ? 4 : 1; + CORRADE_COMPARE(storage.alignment(), alignment); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(mipViews[i]), TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image2DMipmapsIncomplete() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-mipmaps-incomplete.ktx2"))); + + const auto mip0 = Containers::arrayCast(PatternRgbData[0]); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Containers::ArrayView mipViews[2]{mip0, mip1}; + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), Containers::arraySize(mipViews)); + + Vector2i mipSize{4, 3}; + for(UnsignedInt i = 0; i != importer->image2DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image2D(0, i); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(mipViews[i]), TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image2DLayers() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-layers.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{4, 3, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(PatternRgbData), TestSuite::Compare::Container); +} + +void KtxImporterTest::image2DMipmapsAndLayers() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-mipmaps-and-layers.ktx2"))); + + const auto mip0 = Containers::arrayCast(PatternRgbData); + /* Mip data generated by PVRTexTool since it doesn't allow specifying our + own mip data. toktx doesn't seem to support array textures at all, so + this is our best option. Colors were extracted with an external viewer. */ + const Color3ub mip1[2*1*3]{ + 0x0000ff_rgb, 0x7f007f_rgb, + 0x0000ff_rgb, 0x7f007f_rgb, + 0x000000_rgb, 0x000000_rgb + }; + const Color3ub mip2[1*1*3]{ + 0x0000ff_rgb, + 0x0000ff_rgb, + 0x000000_rgb + }; + const Containers::ArrayView mipViews[3]{mip0, mip1, mip2}; + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), Containers::arraySize(mipViews)); + + Vector2i mipSize{4, 3}; + for(UnsignedInt i = 0; i != importer->image3DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image3D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{mipSize, 3})); + + const PixelStorage storage = image->storage(); + /* Alignment is 4 when row length is a multiple of 4 */ + const Int alignment = ((mipSize.x()*image->pixelSize())%4 == 0) ? 4 : 1; + CORRADE_COMPARE(storage.alignment(), alignment); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(mipViews[i]), TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image2DCompressed() { + auto&& data = CompressedImage2DData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 1); + + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), data.format); + CORRADE_COMPARE(image->size(), data.size); + + const CompressedPixelStorage storage = image->compressedStorage(); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + const Vector3i blockSize = compressedBlockSize(data.format); + const Vector3i blockCount = (Vector3i::pad(data.size, 1) + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(data.format)); + CORRADE_COMPARE_AS(std::string(image->data().data(), image->data().size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::Directory::splitExtension(data.file).first + ".bin"), + TestSuite::Compare::StringToFile); +} + +void KtxImporterTest::image2DCompressedMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-mipmaps.ktx2"))); + + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), 4); + + Vector2i mipSize{9, 10}; + for(UnsignedInt i = 0; i != importer->image2DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image2D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + + const Vector3i blockSize = compressedBlockSize(image->compressedFormat()); + const Vector3i blockCount = (Vector3i::pad(mipSize, 1) + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(image->compressedFormat())); + CORRADE_COMPARE_AS(std::string(image->data().data(), image->data().size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::formatString("2d-compressed-mipmaps-mip{}.bin", i)), + TestSuite::Compare::StringToFile); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image2DCompressedLayers() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-layers.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{9, 10, 2})); + + const Vector3i blockSize = compressedBlockSize(image->compressedFormat()); + const Vector3i blockCount = (Vector3i::pad(image->size(), 1) + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(image->compressedFormat())); + CORRADE_COMPARE_AS(std::string(image->data().data(), image->data().size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-layers.bin"), + TestSuite::Compare::StringToFile); +} + +/* Origin bottom-left. There's some weird color shift happening in the test + files, probably the sampling in PVRTexTool. Non-white pixels in the original + files are multiples of 0x101010. */ +const Color3ub FacesRgbData[2][6][2][2]{ + /* cube+x.png */ + {{{0xffffff_rgb, 0x0d0d0d_rgb}, + {0x0d0d0d_rgb, 0x0d0d0d_rgb}}, + /* cube-x.png */ + {{0xffffff_rgb, 0x222222_rgb}, + {0x222222_rgb, 0x222222_rgb}}, + /* cube+y.png */ + {{0xffffff_rgb, 0x323232_rgb}, + {0x323232_rgb, 0x323232_rgb}}, + /* cube-y.png */ + {{0xffffff_rgb, 0x404040_rgb}, + {0x404040_rgb, 0x404040_rgb}}, + /* cube+z.png */ + {{0xffffff_rgb, 0x4f4f4f_rgb}, + {0x4f4f4f_rgb, 0x4f4f4f_rgb}}, + /* cube-z.png */ + {{0xffffff_rgb, 0x606060_rgb}, + {0x606060_rgb, 0x606060_rgb}}}, + + /* cube+z.png */ + {{{0xffffff_rgb, 0x4f4f4f_rgb}, + {0x4f4f4f_rgb, 0x4f4f4f_rgb}}, + /* cube-z.png */ + {{0xffffff_rgb, 0x606060_rgb}, + {0x606060_rgb, 0x606060_rgb}}, + /* cube+x.png */ + {{0xffffff_rgb, 0x0d0d0d_rgb}, + {0x0d0d0d_rgb, 0x0d0d0d_rgb}}, + /* cube-x.png */ + {{0xffffff_rgb, 0x222222_rgb}, + {0x222222_rgb, 0x222222_rgb}}, + /* cube+y.png */ + {{0xffffff_rgb, 0x323232_rgb}, + {0x323232_rgb, 0x323232_rgb}}, + /* cube-y.png */ + {{0xffffff_rgb, 0x404040_rgb}, + {0x404040_rgb, 0x404040_rgb}}} +}; + +void KtxImporterTest::imageCubeMapIncomplete() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "cubemap.ktx2")); + CORRADE_VERIFY(fileData.size() >= sizeof(Implementation::KtxHeader)); + + /* All 6 bits set, should still emit a warning because the check only happens + when face count is not 6 */ + const char data[1]{0x3f}; + /* Not a string, so no terminating 0 */ + const auto keyValueData = createKeyValueData("KTXcubemapIncomplete"_s, Containers::arrayView(data)); + patchKeyValueData(keyValueData, fileData); + + Implementation::KtxHeader& header = *reinterpret_cast(fileData.data()); + header.layerCount = Utility::Endianness::littleEndian(6u); + header.faceCount = Utility::Endianness::littleEndian(1u); + + std::ostringstream outWarning; + Warning redirectWarning{&outWarning}; + + CORRADE_VERIFY(importer->openData(fileData)); + CORRADE_COMPARE(outWarning.str(), + "Trade::KtxImporter::openData(): missing or invalid orientation, assuming right, down\n" + "Trade::KtxImporter::openData(): image contains incomplete cube map faces, importing faces as array layers\n"); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + const auto texture = importer->texture(0); + CORRADE_VERIFY(texture); + CORRADE_COMPARE(texture->type(), TextureType::Texture2DArray); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{2, 2, 6})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 1); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(FacesRgbData[0]), TestSuite::Compare::Container); +} + +void KtxImporterTest::imageCubeMap() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "cubemap.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{2, 2, 6})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 1); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(FacesRgbData[0]), TestSuite::Compare::Container); +} + +void KtxImporterTest::imageCubeMapMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "cubemap-mipmaps.ktx2"))); + + const auto mip0 = Containers::arrayCast(FacesRgbData[0]); + const Color3ub mip1[1*1*6]{ + FacesRgbData[0][0][1][0], + FacesRgbData[0][1][1][0], + FacesRgbData[0][2][1][0], + FacesRgbData[0][3][1][0], + FacesRgbData[0][4][1][0], + FacesRgbData[0][5][1][0] + }; + const Containers::ArrayView mipViews[2]{mip0, mip1}; + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), Containers::arraySize(mipViews)); + + Vector2i mipSize{2, 2}; + for(UnsignedInt i = 0; i != importer->image3DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image3D(0, i); + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{mipSize, 6})); + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(mipViews[i]), TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::imageCubeMapLayers() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "cubemap-layers.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + constexpr UnsignedInt NumLayers = 2; + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{2, 2, NumLayers*6})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 1); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + const UnsignedInt faceSize = image->data().size()/NumLayers; + + for(UnsignedInt i = 0; i != NumLayers; ++i) { + CORRADE_ITERATION(i); + CORRADE_COMPARE_AS(image->data().suffix(i*faceSize).prefix(faceSize), Containers::arrayCast(FacesRgbData[i]), TestSuite::Compare::Container); + } +} + +void KtxImporterTest::image3D() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{4, 3, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + /* Same expected data as image2DLayers but the input images were created + with reversed slice order to account for the z-flip on import from rdi + to ruo */ + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(PatternRgbData), TestSuite::Compare::Container); +} + +void KtxImporterTest::image3DMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-mipmaps.ktx2"))); + + const auto mip0 = Containers::arrayCast(PatternRgbData); + const Color3ub mip1[2]{0xffffff_rgb, 0x007f7f_rgb}; + const Color3ub mip2[1]{0x000000_rgb}; + const Containers::ArrayView mipViews[3]{mip0, mip1, mip2}; + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), Containers::arraySize(mipViews)); + + Vector3i mipSize{4, 3, 3}; + for(UnsignedInt i = 0; i != importer->image3DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image3D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + + const PixelStorage storage = image->storage(); + /* Alignment is 4 when row length is a multiple of 4 */ + const Int alignment = ((mipSize.x()*image->pixelSize())%4 == 0) ? 4 : 1; + CORRADE_COMPARE(storage.alignment(), alignment); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(mipViews[i]), TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::image3DLayers() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-layers.ktx2"))); + + const auto layer0 = Containers::arrayCast(PatternRgbData); + /* Pattern, black, black */ + Color3ub layer1Data[3][3][4]{}; + Utility::copy(Containers::arrayView(PatternRgbData[0]), layer1Data[0]); + const auto layer1 = Containers::arrayCast(layer1Data); + + const Containers::ArrayView imageViews[2]{layer0, layer1}; + + CORRADE_COMPARE(importer->image3DCount(), Containers::arraySize(imageViews)); + + for(UnsignedInt i = 0; i != importer->image3DCount(); ++i) { + CORRADE_ITERATION(i); + + CORRADE_COMPARE(importer->image3DLevelCount(i), 1); + auto image = importer->image3D(i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB8Srgb); + CORRADE_COMPARE(image->size(), (Vector3i{4, 3, 3})); + + const PixelStorage storage = image->storage(); + CORRADE_COMPARE(storage.alignment(), 4); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + CORRADE_COMPARE_AS(image->data(), Containers::arrayCast(imageViews[i]), TestSuite::Compare::Container); + } +} + +void KtxImporterTest::image3DCompressed() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 1); + + auto image = importer->image3D(0); + CORRADE_VERIFY(image); + + constexpr CompressedPixelFormat format = CompressedPixelFormat::Etc2RGB8Srgb; + constexpr Vector3i size{9, 10, 3}; + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), format); + CORRADE_COMPARE(image->size(), size); + + const CompressedPixelStorage storage = image->compressedStorage(); + CORRADE_COMPARE(storage.rowLength(), 0); + CORRADE_COMPARE(storage.imageHeight(), 0); + CORRADE_COMPARE(storage.skip(), Vector3i{}); + + const Vector3i blockSize = compressedBlockSize(format); + const Vector3i blockCount = (size + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(format)); + CORRADE_COMPARE_AS(std::string(image->data().data(), image->data().size()), + Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed.bin"), + TestSuite::Compare::StringToFile); +} + +void KtxImporterTest::image3DCompressedMipmaps() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "3d-compressed-mipmaps.ktx2"))); + + CORRADE_COMPARE(importer->image3DCount(), 1); + CORRADE_COMPARE(importer->image3DLevelCount(0), 4); + + Vector3i mipSize{9, 10, 5}; + for(UnsignedInt i = 0; i != importer->image3DLevelCount(0); ++i) { + CORRADE_ITERATION(i); + + auto image = importer->image3D(0, i); + CORRADE_VERIFY(image); + + CORRADE_VERIFY(image->isCompressed()); + CORRADE_COMPARE(image->compressedFormat(), CompressedPixelFormat::Etc2RGB8Srgb); + CORRADE_COMPARE(image->size(), mipSize); + + const Vector3i blockSize = compressedBlockSize(image->compressedFormat()); + const Vector3i blockCount = (mipSize + (blockSize - Vector3i{1}))/blockSize; + CORRADE_COMPARE(image->data().size(), blockCount.product()*compressedBlockDataSize(image->compressedFormat())); + /* Compressed .bin data is manually generated in generate.sh, don't + need to save it like the 1D/2D files */ + const auto data = Utility::Directory::read( + Utility::Directory::join(KTXIMPORTER_TEST_DIR, Utility::formatString("3d-compressed-mipmaps-mip{}.bin", i))); + CORRADE_COMPARE_AS(image->data(), data, TestSuite::Compare::Container); + + mipSize = Math::max(mipSize >> 1, 1); + } +} + +void KtxImporterTest::keyValueDataEmpty() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2")); + CORRADE_VERIFY(fileData.size() >= sizeof(Implementation::KtxHeader)); + + Implementation::KtxHeader& header = *reinterpret_cast(fileData.data()); + header.kvdByteLength = Utility::Endianness::littleEndian(0u); + + std::ostringstream outWarning; + Warning redirectWarning{&outWarning}; + + CORRADE_VERIFY(importer->openData(fileData)); + /* This test doubles for empty orientation data, but there should be no + other warnings */ + CORRADE_COMPARE(outWarning.str(), "Trade::KtxImporter::openData(): missing or invalid orientation, assuming right, down\n"); +} + +void KtxImporterTest::keyValueDataInvalid() { + auto&& data = InvalidKeyValueData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Invalid key/value data that might hint at a broken file so the importer + should warn and try to continue the import */ + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2")); + + patchKeyValueData(data.data, fileData); + + std::ostringstream outWarning; + Warning redirectWarning{&outWarning}; + + /* Import succeeds with a warning */ + CORRADE_VERIFY(importer->openData(fileData)); + CORRADE_COMPARE(outWarning.str(), Utility::formatString( + "Trade::KtxImporter::openData(): {}\n" + "Trade::KtxImporter::openData(): missing or invalid orientation, assuming right, down\n", + data.message)); +} + +void KtxImporterTest::keyValueDataInvalidIgnored() { + auto&& data = IgnoredInvalidKeyValueData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + /* Invalid (according to the spec) key/value data that can just be + ignored without warning because it doesn't affect the import */ + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2")); + + patchKeyValueData(data.data, fileData); + + std::ostringstream outWarning; + Warning redirectWarning{&outWarning}; + + /* No warning besides missing orientation */ + CORRADE_VERIFY(importer->openData(fileData)); + CORRADE_COMPARE(outWarning.str(), "Trade::KtxImporter::openData(): missing or invalid orientation, assuming right, down\n"); +} + +void KtxImporterTest::orientationInvalid() { + auto&& data = InvalidOrientationData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + patchKeyValueData(createKeyValueData("KTXorientation"_s, data.orientation), fileData); + + std::ostringstream outWarning; + Warning redirectWarning{&outWarning}; + CORRADE_VERIFY(importer->openData(fileData)); + + constexpr Containers::StringView orientations[]{"right"_s, "down"_s, "forward"_s}; + const Containers::String orientationString = ", "_s.join(Containers::arrayView(orientations).prefix(data.dimensions)); + CORRADE_COMPARE(outWarning.str(), Utility::formatString("Trade::KtxImporter::openData(): missing or invalid orientation, assuming {}\n", orientationString)); +} + +void KtxImporterTest::orientationFlip() { + auto&& data = FlipData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + patchKeyValueData(createKeyValueData("KTXorientation"_s, data.name), fileData); + + CORRADE_VERIFY(importer->openData(fileData)); + + const Vector3i size = Math::max(data.size, 1); + const Int dimensions = Math::min(data.size, 1).sum(); + Containers::Array imageData; + switch(dimensions) { + case 1: { + const auto image = importer->image1D(0); + imageData = Containers::Array{image->data().size()}; + Utility::copy(image->data(), imageData); + break; + } + case 2: { + const auto image = importer->image2D(0); + imageData = Containers::Array{image->data().size()}; + Utility::copy(image->data(), imageData); + break; + } + case 3: { + const auto image = importer->image3D(0); + imageData = Containers::Array{image->data().size()}; + Utility::copy(image->data(), imageData); + break; + } + default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + } + + Containers::StridedArrayView4D src{imageData, { + std::size_t(size.z()), + std::size_t(size.y()), + std::size_t(size.x()), + pixelSize(data.format) + }}; + + Containers::Array flippedData{imageData.size()}; + Containers::StridedArrayView4D dst{flippedData, src.size()}; + + if(data.flipped[2]) src = src.flipped<0>(); + if(data.flipped[1]) src = src.flipped<1>(); + if(data.flipped[0]) src = src.flipped<2>(); + + Utility::copy(src, dst); + + CORRADE_COMPARE_AS(data.data, flippedData, TestSuite::Compare::Container); +} + +void KtxImporterTest::orientationFlipCompressed() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + /* Just check for the warning, image2DCompressed checks that the output is + as expected */ + + std::ostringstream outWarning; + Warning redirectWarning{&outWarning}; + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-bc1.ktx2"))); + CORRADE_COMPARE(outWarning.str(), + "Trade::KtxImporter::openData(): block-compressed image " + "was encoded with non-default axis orientations, imported data " + "will have wrong orientation\n"); +} + +void KtxImporterTest::swizzle() { + auto&& data = SwizzleData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + importer->addFlags(ImporterFlag::Verbose); + + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, data.file)); + CORRADE_VERIFY(fileData.size() > sizeof(Implementation::KtxHeader)); + + /* toktx lets us swizzle the input data, but doesn't turn the format into + a swizzled one. Patch the header manually. */ + if(data.vkFormat != Implementation::VK_FORMAT_UNDEFINED) { + auto& header = *reinterpret_cast(fileData.data()); + header.vkFormat = Utility::Endianness::littleEndian(data.vkFormat); + } + + std::ostringstream outDebug; + Debug redirectDebug{&outDebug}; + + CORRADE_VERIFY(importer->openData(fileData)); + + std::string expectedMessage = "Trade::KtxImporter::openData(): image will be flipped along y\n"; + if(data.message) + expectedMessage += Utility::formatString("Trade::KtxImporter::openData(): {}\n", data.message); + CORRADE_COMPARE(outDebug.str(), expectedMessage); + + CORRADE_COMPARE(importer->image2DCount(), 1); + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->format(), data.format); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + CORRADE_COMPARE_AS(image->data(), data.data, TestSuite::Compare::Container); +} + +void KtxImporterTest::swizzleMultipleBytes() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + importer->addFlags(ImporterFlag::Verbose); + + std::ostringstream outDebug; + Debug redirectDebug{&outDebug}; + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "bgr-swizzle-bgr-16bit.ktx2"))); + + CORRADE_COMPARE(outDebug.str(), + "Trade::KtxImporter::openData(): image will be flipped along y\n" + "Trade::KtxImporter::openData(): format requires conversion from BGR to RGB\n"); + + /* For some reason a 16-bit PNG sent through toktx ends up with 8-bit + channels duplicated to 16 bits instead of being remapped. Not sure if + this is a bug in GIMP or toktx, although the PNG shows correctly in + several viewers so probably the latter. PVRTexTool does the same thing, + see imageRgb32U(). This is PatternRgbData[0], but each byte extended to + unsigned short by just repeating the byte twice. */ + constexpr UnsignedShort Half = 0x7f7f; + constexpr Math::Color3 content[4*3]{ + {0xffff, 0, 0}, {0xffff, 0xffff, 0xffff}, { 0, 0, 0}, { 0, 0xffff, 0}, + {0xffff, 0xffff, 0xffff}, {0xffff, 0, 0}, { 0, 0, 0}, { 0, 0xffff, 0}, + { 0, 0, 0xffff}, { 0, 0xffff, 0}, {Half, 0, Half}, {Half, 0, Half} + }; + + CORRADE_COMPARE(importer->image2DCount(), 1); + auto image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->format(), PixelFormat::RGB16Unorm); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + CORRADE_COMPARE_AS(Containers::arrayCast>(image->data()), + Containers::arrayView(content), TestSuite::Compare::Container); +} + +void KtxImporterTest::swizzleIdentity() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + importer->addFlags(ImporterFlag::Verbose); + + std::ostringstream out; + Debug redirectError{&out}; + + /* RGB1 swizzle. This also checks that the correct prefix based on channel + count is used, since swizzle is always a constant length 4 in the + key/value data. */ + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "swizzle-identity.ktx2"))); + /* No message about format requiring conversion */ + CORRADE_COMPARE(out.str(), "Trade::KtxImporter::openData(): image will be flipped along y\n"); +} + +void KtxImporterTest::swizzleUnsupported() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + std::ostringstream out; + Error redirectError{&out}; + + /* Only identity (RG?B?A?), BGR and BGRA swizzle supported. This is the same + swizzle string as in swizzle-identity.ktx2, but this file is RGBA instead + of RGB, so the 1 shouldn't be ignored. */ + CORRADE_VERIFY(!importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "swizzle-unsupported.ktx2"))); + CORRADE_COMPARE(out.str(), "Trade::KtxImporter::openData(): unsupported channel mapping rgb1\n"); +} + +void KtxImporterTest::swizzleCompressed() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + auto fileData = Utility::Directory::read(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-compressed-bc1.ktx2")); + patchKeyValueData(createKeyValueData("KTXswizzle"_s, "bgra"_s), fileData); + + std::ostringstream out; + Error redirectError{&out}; + + CORRADE_VERIFY(!importer->openData(fileData)); + CORRADE_COMPARE(out.str(), "Trade::KtxImporter::openData(): unsupported channel mapping bgra\n"); +} + +void KtxImporterTest::openTwice() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2"))); + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->textureCount(), 1); + + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2"))); + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->textureCount(), 1); + + /* Shouldn't crash, leak or anything */ +} + +void KtxImporterTest::importTwice() { + Containers::Pointer importer = _manager.instantiate("KtxImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Directory::join(KTXIMPORTER_TEST_DIR, "2d-rgb.ktx2"))); + + /* Verify that everything is working the same way on second use */ + { + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + } { + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->size(), (Vector2i{4, 3})); + } +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Trade::Test::KtxImporterTest) diff --git a/src/MagnumPlugins/KtxImporter/Test/README.md b/src/MagnumPlugins/KtxImporter/Test/README.md new file mode 100644 index 000000000..f85ded477 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/Test/README.md @@ -0,0 +1,18 @@ +Updating test files +=================== + +Most of the `*.ktx2?` files are created using [Khronos Texture Tools](https://github.com/KhronosGroup/KTX-Software) as well as [PVRTexTool](https://developer.imaginationtech.com/pvrtextool/). + +Install both of those and then execute the `generate.sh` script to create the test files. + +The remaining files (various .ktx2 files, .bin files for compressed block data) are generated by running both KtxImageConverterTest and KtxImporterTest with `--save-diagnostic [path/to/this/folder]` until they stop failing. The order should be: + +```bash +# Generates 1D and 2D .bin files +# Needs to be run multiple times until all mips are saved +KtxImporterTest -S [path/to/KtxImporter/Test] +# Generates depth/stencil .ktx2 and 3d[-compressed]-mipmaps.ktx +KtxImageConverterTest -S [path/to/KtxImporter/Test] +``` + +Use a third-party viewer to make sure the .ktx2 files load correctly and look plausible. diff --git a/src/MagnumPlugins/KtxImporter/Test/bgr-swizzle-bgr-16bit.ktx2 b/src/MagnumPlugins/KtxImporter/Test/bgr-swizzle-bgr-16bit.ktx2 new file mode 100644 index 000000000..593ee26bd Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/bgr-swizzle-bgr-16bit.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/bgr-swizzle-bgr.ktx2 b/src/MagnumPlugins/KtxImporter/Test/bgr-swizzle-bgr.ktx2 new file mode 100644 index 000000000..4fe75fce6 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/bgr-swizzle-bgr.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/bgr.ktx2 b/src/MagnumPlugins/KtxImporter/Test/bgr.ktx2 new file mode 100644 index 000000000..a6d482e4b Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/bgr.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/bgra-swizzle-bgra.ktx2 b/src/MagnumPlugins/KtxImporter/Test/bgra-swizzle-bgra.ktx2 new file mode 100644 index 000000000..2762d066b Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/bgra-swizzle-bgra.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/bgra.ktx2 b/src/MagnumPlugins/KtxImporter/Test/bgra.ktx2 new file mode 100644 index 000000000..8bcb81107 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/bgra.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/black-1d.png b/src/MagnumPlugins/KtxImporter/Test/black-1d.png new file mode 100644 index 000000000..8a84c24e3 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/black-1d.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/black.png b/src/MagnumPlugins/KtxImporter/Test/black.png new file mode 100644 index 000000000..5e62ca32b Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/black.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/configure.h.cmake b/src/MagnumPlugins/KtxImporter/Test/configure.h.cmake new file mode 100644 index 000000000..b10586f4e --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/Test/configure.h.cmake @@ -0,0 +1,28 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#cmakedefine KTXIMPORTER_PLUGIN_FILENAME "${KTXIMPORTER_PLUGIN_FILENAME}" +#define KTXIMPORTER_TEST_DIR "${KTXIMPORTER_TEST_DIR}" diff --git a/src/MagnumPlugins/KtxImporter/Test/cube+x.png b/src/MagnumPlugins/KtxImporter/Test/cube+x.png new file mode 100644 index 000000000..47fb8238b Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cube+x.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cube+y.png b/src/MagnumPlugins/KtxImporter/Test/cube+y.png new file mode 100644 index 000000000..c5ecc105c Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cube+y.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cube+z.png b/src/MagnumPlugins/KtxImporter/Test/cube+z.png new file mode 100644 index 000000000..5949b6c8f Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cube+z.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cube-x.png b/src/MagnumPlugins/KtxImporter/Test/cube-x.png new file mode 100644 index 000000000..2f501b867 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cube-x.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cube-y.png b/src/MagnumPlugins/KtxImporter/Test/cube-y.png new file mode 100644 index 000000000..ea69c56df Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cube-y.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cube-z.png b/src/MagnumPlugins/KtxImporter/Test/cube-z.png new file mode 100644 index 000000000..b88d5e0b2 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cube-z.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cubemap-layers.ktx2 b/src/MagnumPlugins/KtxImporter/Test/cubemap-layers.ktx2 new file mode 100644 index 000000000..8aafbc6ab Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cubemap-layers.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cubemap-mipmaps.ktx2 b/src/MagnumPlugins/KtxImporter/Test/cubemap-mipmaps.ktx2 new file mode 100644 index 000000000..69865d0ce Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cubemap-mipmaps.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/cubemap.ktx2 b/src/MagnumPlugins/KtxImporter/Test/cubemap.ktx2 new file mode 100644 index 000000000..4e01ee515 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/cubemap.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/generate.sh b/src/MagnumPlugins/KtxImporter/Test/generate.sh new file mode 100644 index 000000000..80f019126 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/Test/generate.sh @@ -0,0 +1,129 @@ +#!/bin/bash + +set -e + +# The Khronos tools don't support array, cubemap or 3D textures, so we need +# PVRTexTool as well. That doesn't support 3D textures, but we can patch the +# header of 2D array textures to turn them into 3D textures. +# Not a fan of using closed-source tools, but it works... +# https://github.com/KhronosGroup/KTX-Software v4.0.0 +# https://developer.imaginationtech.com/pvrtextool/ v5.1.0 + +toktx version1.ktx black.png + +toktx --t2 --swizzle rgb1 swizzle-identity.ktx2 pattern.png +toktx --t2 --target_type RGBA --swizzle rgb1 swizzle-unsupported.ktx2 pattern.png + +# swizzled data, same swizzle in header +# data should come out normally after the importer swizzles it +toktx --t2 --input_swizzle bgra --swizzle bgr1 bgr-swizzle-bgr.ktx2 pattern.png +toktx --t2 --target_type RGBA --input_swizzle bgra --swizzle bgra bgra-swizzle-bgra.ktx2 pattern.png +toktx --t2 --input_swizzle bgra --swizzle bgr1 bgr-swizzle-bgr-16bit.ktx2 pattern-16bit.png + +# swizzled header +# with patched swizzled vkFormat data should come out normally because both cancel each other out +toktx --t2 --swizzle bgr1 swizzle-bgr.ktx2 pattern.png +toktx --t2 --target_type RGBA --swizzle bgra swizzle-bgra.ktx2 pattern.png + +# swizzled data +# with patched swizzled vkFormat data should come out normally +toktx --t2 --input_swizzle bgra bgr.ktx2 pattern.png +toktx --t2 --target_type RGBA --input_swizzle bgra bgra.ktx2 pattern.png + +# 2D +toktx --t2 2d-rgb.ktx2 pattern.png +toktx --t2 --target_type RGBA 2d-rgba.ktx2 pattern.png + +# A few interesting formats +# PVRTexTool lets us export to different formats although the documentation +# isn't very clear on how it converts +PVRTexToolCLI -i pattern.png -o 2d-rgb32.ktx2 -f r32g32b32,UI,sRGB +PVRTexToolCLI -i pattern.png -o 2d-rgbf32.ktx2 -f r32g32b32,SF,sRGB + +# manual mipmaps, full pyramid and one without the last +toktx --t2 --mipmap 2d-mipmaps.ktx2 pattern.png pattern-mip1.png pattern-mip2.png +toktx --t2 --mipmap --levels 2 2d-mipmaps-incomplete.ktx2 pattern.png pattern-mip1.png + +# layers +PVRTexToolCLI -i pattern.png,pattern.png,black.png -o 2d-layers.ktx2 -array -f r8g8b8,UBN,sRGB + +# mipmaps and layers +# PVRTexTool doesn't let us specify mip images manually +PVRTexToolCLI -i pattern.png,pattern.png,black.png -o 2d-mipmaps-and-layers.ktx2 -array -m -mfilter nearest -f r8g8b8,UBN,sRGB + +# cube map +PVRTexToolCLI -i cube+x.png,cube-x.png,cube+y.png,cube-y.png,cube+z.png,cube-z.png -o cubemap.ktx2 -cube -f r8g8b8,UBN,sRGB +PVRTexToolCLI -i cube+x.png,cube-x.png,cube+y.png,cube-y.png,cube+z.png,cube-z.png -o cubemap-mipmaps.ktx2 -cube -m -mfilter nearest -f r8g8b8,UBN,sRGB +# layered cube map: faces for layer 0, then layer 1 +PVRTexToolCLI -i cube+x.png,cube-x.png,cube+y.png,cube-y.png,cube+z.png,cube-z.png,cube+z.png,cube-z.png,cube+x.png,cube-x.png,cube+y.png,cube-y.png -o cubemap-layers.ktx2 -cube -array -f r8g8b8,UBN,sRGB + +# 1D +toktx --t2 1d.ktx2 pattern-1d.png +toktx --t2 --mipmap 1d-mipmaps.ktx2 pattern-1d.png pattern-mip1.png pattern-mip2.png +PVRTexToolCLI -i pattern-1d.png,pattern-1d.png,black-1d.png -o 1d-layers.ktx2 -array -f r8g8b8,UBN,sRGB + +# 3D +# PVRTexTool doesn't support 3D images, sigh +# Create a layered image and patch the header to make it 3D. The level layout +# is identical to a 3D image, but the orientation metadata will become invalid. +# The importer will ignore invalid orientation but for correct ground-truth +# data for converter tests we need to patch it. +# Because the z axis will be flipped, we need to pass the images in reverse +# order compared to 2d-layers.ktx2. +PVRTexToolCLI -i black.png,pattern.png,pattern.png -o 3d.ktx2 -array -f r8g8b8,UBN,sRGB +# depth = 3, numLayers = 0 +printf '\x03\x00\x00\x00\x00\x00\x00\x00' | dd conv=notrunc of=3d.ktx2 bs=1 seek=28 +# KTXorientation length +printf '\x13' | dd conv=notrunc of=3d.ktx2 bs=1 seek=180 +# KTXorientation, replace first padding byte +printf 'i' | dd conv=notrunc of=3d.ktx2 bs=1 seek=201 + +# We can't patch a 2D array texture with mipmaps into a 3D texture because the +# number of layers stays the same, unlike shrinking z in mipmap levels +# 3d-mipmaps.ktx2 and 3d-compressed-mipmaps.ktx2 are generated by running +# KtxImageConverterTest --save-diagnostic +PVRTexToolCLI -i black.png,pattern.png,pattern.png,black.png,black.png,pattern.png -o 3d-layers.ktx2 -array -f r8g8b8,UBN,sRGB +printf '\x03\x00\x00\x00\x02\x00\x00\x00' | dd conv=notrunc of=3d-layers.ktx2 bs=1 seek=28 +# TODO: patch up KTXorientation for 3d-layers.ktx2 if we need it for the converter tests + +# Compressed +# PVRTC and BC* don't support non-power-of-2 +PVRTexToolCLI -i pattern-pot.png -o 2d-compressed-pvrtc.ktx2 -f PVRTC1_4,UBN,sRGB +PVRTexToolCLI -i pattern-pot.png -o 2d-compressed-bc1.ktx2 -f BC1,UBN,sRGB +PVRTexToolCLI -i pattern-pot.png -o 2d-compressed-bc3.ktx2 -f BC3,UBN,sRGB +PVRTexToolCLI -i pattern-uneven.png -o 2d-compressed-etc2.ktx2 -f ETC2_RGB,UBN,sRGB +PVRTexToolCLI -i pattern-uneven.png -o 2d-compressed-astc.ktx2 -f ASTC_12x10,UBN,sRGB + +PVRTexToolCLI -i pattern-uneven.png -o 2d-compressed-mipmaps.ktx2 -m -mfilter nearest -f ETC2_RGB,UBN,sRGB +PVRTexToolCLI -i pattern-uneven.png,black.png -o 2d-compressed-layers.ktx2 -array -f ETC2_RGB,UBN,sRGB + +PVRTexToolCLI -i pattern-1d.png -o 1d-compressed-bc1.ktx2 -f BC1,UBN,sRGB +PVRTexToolCLI -i pattern-1d-uneven.png -o 1d-compressed-etc2.ktx2 -f ETC2_RGB,UBN,sRGB + +PVRTexToolCLI -i pattern-1d-uneven.png -o 1d-compressed-mipmaps.ktx2 -m -mfilter nearest -f ETC2_RGB,UBN,sRGB + +PVRTexToolCLI -i pattern-uneven.png,pattern-uneven.png,black.png -o 3d-compressed.ktx2 -array -f ETC2_RGB,UBN,sRGB +printf '\x03\x00\x00\x00\x00\x00\x00\x00' | dd conv=notrunc of=3d-compressed.ktx2 bs=1 seek=28 +printf '\x13' | dd conv=notrunc of=3d-compressed.ktx2 bs=1 seek=148 +printf 'i' | dd conv=notrunc of=3d-compressed.ktx2 bs=1 seek=169 + +# Unlike 3d-mipmaps.ktx2, we don't have any verifiable data to check in viewers +# for 3d-compressed-mipmaps.ktx to make sure the converter output is plausible. +# Instead of randomly generating bytes, some of the existing 2D ETC2 .bin files +# are used to manually create the mipmaps. A hack, but better than nothing. +# We can simply repeat all the blocks for each z-slice. +# Last slice (if any) is all zeros to check the order. +yes 2d-compressed-mipmaps-mip0.bin | head -n 4 | xargs cat > 3d-compressed-mipmaps-mip0.bin +size=$(stat -c%s 2d-compressed-mipmaps-mip0.bin) +dd if=/dev/zero bs=1 count=$size >> 3d-compressed-mipmaps-mip0.bin + +cp 2d-compressed-mipmaps-mip1.bin 3d-compressed-mipmaps-mip1.bin +size=$(stat -c%s 2d-compressed-mipmaps-mip1.bin) +dd if=/dev/zero bs=1 count=$size >> 3d-compressed-mipmaps-mip1.bin + +cp 2d-compressed-mipmaps-mip2.bin 3d-compressed-mipmaps-mip2.bin +cp 2d-compressed-mipmaps-mip3.bin 3d-compressed-mipmaps-mip3.bin + +# TODO: +# 3D (compressed) mips so we don't have to generate our own in the converter tests +# Should be possible once https://github.com/KhronosGroup/KTX-Software/pull/468 made it into a release diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-16bit.png b/src/MagnumPlugins/KtxImporter/Test/pattern-16bit.png new file mode 100644 index 000000000..8d56f89db Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-16bit.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-1d-uneven.png b/src/MagnumPlugins/KtxImporter/Test/pattern-1d-uneven.png new file mode 100644 index 000000000..cdfd6b9bc Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-1d-uneven.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-1d.png b/src/MagnumPlugins/KtxImporter/Test/pattern-1d.png new file mode 100644 index 000000000..176b2b2c6 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-1d.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-mip1.png b/src/MagnumPlugins/KtxImporter/Test/pattern-mip1.png new file mode 100644 index 000000000..a08eab059 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-mip1.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-mip2.png b/src/MagnumPlugins/KtxImporter/Test/pattern-mip2.png new file mode 100644 index 000000000..706ddf82a Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-mip2.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-pot.png b/src/MagnumPlugins/KtxImporter/Test/pattern-pot.png new file mode 100644 index 000000000..99f8dc837 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-pot.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern-uneven.png b/src/MagnumPlugins/KtxImporter/Test/pattern-uneven.png new file mode 100644 index 000000000..a14d69394 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern-uneven.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/pattern.png b/src/MagnumPlugins/KtxImporter/Test/pattern.png new file mode 100644 index 000000000..65beef670 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/pattern.png differ diff --git a/src/MagnumPlugins/KtxImporter/Test/swizzle-bgr.ktx2 b/src/MagnumPlugins/KtxImporter/Test/swizzle-bgr.ktx2 new file mode 100644 index 000000000..bd1dbd874 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/swizzle-bgr.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/swizzle-bgra.ktx2 b/src/MagnumPlugins/KtxImporter/Test/swizzle-bgra.ktx2 new file mode 100644 index 000000000..722cc1ccd Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/swizzle-bgra.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/swizzle-identity.ktx2 b/src/MagnumPlugins/KtxImporter/Test/swizzle-identity.ktx2 new file mode 100644 index 000000000..108847430 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/swizzle-identity.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/swizzle-unsupported.ktx2 b/src/MagnumPlugins/KtxImporter/Test/swizzle-unsupported.ktx2 new file mode 100644 index 000000000..a2040dda2 Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/swizzle-unsupported.ktx2 differ diff --git a/src/MagnumPlugins/KtxImporter/Test/version1.ktx b/src/MagnumPlugins/KtxImporter/Test/version1.ktx new file mode 100644 index 000000000..d8ce8e4de Binary files /dev/null and b/src/MagnumPlugins/KtxImporter/Test/version1.ktx differ diff --git a/src/MagnumPlugins/KtxImporter/compressedFormatMapping.hpp b/src/MagnumPlugins/KtxImporter/compressedFormatMapping.hpp new file mode 100644 index 000000000..5c3f3024e --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/compressedFormatMapping.hpp @@ -0,0 +1,103 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* Autogenerated from formatMapping.py! Do not edit! */ + +/* VkFormat, CompressedPixelFormat, Implementation::VkFormatSuffix */ +#ifdef _c +_c(131, Bc1RGBUnorm, UNORM) /* VK_FORMAT_BC1_RGB_UNORM_BLOCK */ +_c(132, Bc1RGBSrgb, SRGB) /* VK_FORMAT_BC1_RGB_SRGB_BLOCK */ +_c(133, Bc1RGBAUnorm, UNORM) /* VK_FORMAT_BC1_RGBA_UNORM_BLOCK */ +_c(134, Bc1RGBASrgb, SRGB) /* VK_FORMAT_BC1_RGBA_SRGB_BLOCK */ +_c(135, Bc2RGBAUnorm, UNORM) /* VK_FORMAT_BC2_UNORM_BLOCK */ +_c(136, Bc2RGBASrgb, SRGB) /* VK_FORMAT_BC2_SRGB_BLOCK */ +_c(137, Bc3RGBAUnorm, UNORM) /* VK_FORMAT_BC3_UNORM_BLOCK */ +_c(138, Bc3RGBASrgb, SRGB) /* VK_FORMAT_BC3_SRGB_BLOCK */ +_c(139, Bc4RUnorm, UNORM) /* VK_FORMAT_BC4_UNORM_BLOCK */ +_c(140, Bc4RSnorm, SNORM) /* VK_FORMAT_BC4_SNORM_BLOCK */ +_c(141, Bc5RGUnorm, UNORM) /* VK_FORMAT_BC5_UNORM_BLOCK */ +_c(142, Bc5RGSnorm, SNORM) /* VK_FORMAT_BC5_SNORM_BLOCK */ +_c(143, Bc6hRGBUfloat, UFLOAT) /* VK_FORMAT_BC6H_UFLOAT_BLOCK */ +_c(144, Bc6hRGBSfloat, SFLOAT) /* VK_FORMAT_BC6H_SFLOAT_BLOCK */ +_c(145, Bc7RGBAUnorm, UNORM) /* VK_FORMAT_BC7_UNORM_BLOCK */ +_c(146, Bc7RGBASrgb, SRGB) /* VK_FORMAT_BC7_SRGB_BLOCK */ +_c(153, EacR11Unorm, UNORM) /* VK_FORMAT_EAC_R11_UNORM_BLOCK */ +_c(154, EacR11Snorm, SNORM) /* VK_FORMAT_EAC_R11_SNORM_BLOCK */ +_c(155, EacRG11Unorm, UNORM) /* VK_FORMAT_EAC_R11G11_UNORM_BLOCK */ +_c(156, EacRG11Snorm, SNORM) /* VK_FORMAT_EAC_R11G11_SNORM_BLOCK */ +_c(147, Etc2RGB8Unorm, UNORM) /* VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK */ +_c(148, Etc2RGB8Srgb, SRGB) /* VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK */ +_c(149, Etc2RGB8A1Unorm, UNORM) /* VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK */ +_c(150, Etc2RGB8A1Srgb, SRGB) /* VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK */ +_c(151, Etc2RGBA8Unorm, UNORM) /* VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK */ +_c(152, Etc2RGBA8Srgb, SRGB) /* VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK */ +_c(157, Astc4x4RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_4x4_UNORM_BLOCK */ +_c(158, Astc4x4RGBASrgb, SRGB) /* VK_FORMAT_ASTC_4x4_SRGB_BLOCK */ +_c(1000066000, Astc4x4RGBAF, SFLOAT) /* VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT */ +_c(159, Astc5x4RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_5x4_UNORM_BLOCK */ +_c(160, Astc5x4RGBASrgb, SRGB) /* VK_FORMAT_ASTC_5x4_SRGB_BLOCK */ +_c(1000066001, Astc5x4RGBAF, SFLOAT) /* VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK_EXT */ +_c(161, Astc5x5RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_5x5_UNORM_BLOCK */ +_c(162, Astc5x5RGBASrgb, SRGB) /* VK_FORMAT_ASTC_5x5_SRGB_BLOCK */ +_c(1000066002, Astc5x5RGBAF, SFLOAT) /* VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK_EXT */ +_c(163, Astc6x5RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_6x5_UNORM_BLOCK */ +_c(164, Astc6x5RGBASrgb, SRGB) /* VK_FORMAT_ASTC_6x5_SRGB_BLOCK */ +_c(1000066003, Astc6x5RGBAF, SFLOAT) /* VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK_EXT */ +_c(165, Astc6x6RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_6x6_UNORM_BLOCK */ +_c(166, Astc6x6RGBASrgb, SRGB) /* VK_FORMAT_ASTC_6x6_SRGB_BLOCK */ +_c(1000066004, Astc6x6RGBAF, SFLOAT) /* VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK_EXT */ +_c(167, Astc8x5RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_8x5_UNORM_BLOCK */ +_c(168, Astc8x5RGBASrgb, SRGB) /* VK_FORMAT_ASTC_8x5_SRGB_BLOCK */ +_c(1000066005, Astc8x5RGBAF, SFLOAT) /* VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK_EXT */ +_c(169, Astc8x6RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_8x6_UNORM_BLOCK */ +_c(170, Astc8x6RGBASrgb, SRGB) /* VK_FORMAT_ASTC_8x6_SRGB_BLOCK */ +_c(1000066006, Astc8x6RGBAF, SFLOAT) /* VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK_EXT */ +_c(171, Astc8x8RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_8x8_UNORM_BLOCK */ +_c(172, Astc8x8RGBASrgb, SRGB) /* VK_FORMAT_ASTC_8x8_SRGB_BLOCK */ +_c(1000066007, Astc8x8RGBAF, SFLOAT) /* VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK_EXT */ +_c(173, Astc10x5RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_10x5_UNORM_BLOCK */ +_c(174, Astc10x5RGBASrgb, SRGB) /* VK_FORMAT_ASTC_10x5_SRGB_BLOCK */ +_c(1000066008, Astc10x5RGBAF, SFLOAT) /* VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK_EXT */ +_c(175, Astc10x6RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_10x6_UNORM_BLOCK */ +_c(176, Astc10x6RGBASrgb, SRGB) /* VK_FORMAT_ASTC_10x6_SRGB_BLOCK */ +_c(1000066009, Astc10x6RGBAF, SFLOAT) /* VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK_EXT */ +_c(177, Astc10x8RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_10x8_UNORM_BLOCK */ +_c(178, Astc10x8RGBASrgb, SRGB) /* VK_FORMAT_ASTC_10x8_SRGB_BLOCK */ +_c(1000066010, Astc10x8RGBAF, SFLOAT) /* VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK_EXT */ +_c(179, Astc10x10RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_10x10_UNORM_BLOCK */ +_c(180, Astc10x10RGBASrgb, SRGB) /* VK_FORMAT_ASTC_10x10_SRGB_BLOCK */ +_c(1000066011, Astc10x10RGBAF, SFLOAT) /* VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK_EXT */ +_c(181, Astc12x10RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_12x10_UNORM_BLOCK */ +_c(182, Astc12x10RGBASrgb, SRGB) /* VK_FORMAT_ASTC_12x10_SRGB_BLOCK */ +_c(1000066012, Astc12x10RGBAF, SFLOAT) /* VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK_EXT */ +_c(183, Astc12x12RGBAUnorm, UNORM) /* VK_FORMAT_ASTC_12x12_UNORM_BLOCK */ +_c(184, Astc12x12RGBASrgb, SRGB) /* VK_FORMAT_ASTC_12x12_SRGB_BLOCK */ +_c(1000066013, Astc12x12RGBAF, SFLOAT) /* VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK_EXT */ +_c(1000054000, PvrtcRGBA2bppUnorm, UNORM) /* VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG */ +_c(1000054004, PvrtcRGBA2bppSrgb, SRGB) /* VK_FORMAT_PVRTC1_2BPP_SRGB_BLOCK_IMG */ +_c(1000054001, PvrtcRGBA4bppUnorm, UNORM) /* VK_FORMAT_PVRTC1_4BPP_UNORM_BLOCK_IMG */ +_c(1000054005, PvrtcRGBA4bppSrgb, SRGB) /* VK_FORMAT_PVRTC1_4BPP_SRGB_BLOCK_IMG */ +#endif diff --git a/src/MagnumPlugins/KtxImporter/configure.h.cmake b/src/MagnumPlugins/KtxImporter/configure.h.cmake new file mode 100644 index 000000000..537f9cca3 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/configure.h.cmake @@ -0,0 +1,27 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#cmakedefine MAGNUM_KTXIMPORTER_BUILD_STATIC diff --git a/src/MagnumPlugins/KtxImporter/formatMapping.hpp b/src/MagnumPlugins/KtxImporter/formatMapping.hpp new file mode 100644 index 000000000..d13b01627 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/formatMapping.hpp @@ -0,0 +1,90 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* Autogenerated from formatMapping.py! Do not edit! */ + +/* VkFormat, PixelFormat, Implementation::VkFormatSuffix */ +#ifdef _c +_c(9, R8Unorm, UNORM) /* VK_FORMAT_R8_UNORM */ +_c(16, RG8Unorm, UNORM) /* VK_FORMAT_R8G8_UNORM */ +_c(23, RGB8Unorm, UNORM) /* VK_FORMAT_R8G8B8_UNORM */ +_c(37, RGBA8Unorm, UNORM) /* VK_FORMAT_R8G8B8A8_UNORM */ +_c(10, R8Snorm, SNORM) /* VK_FORMAT_R8_SNORM */ +_c(17, RG8Snorm, SNORM) /* VK_FORMAT_R8G8_SNORM */ +_c(24, RGB8Snorm, SNORM) /* VK_FORMAT_R8G8B8_SNORM */ +_c(38, RGBA8Snorm, SNORM) /* VK_FORMAT_R8G8B8A8_SNORM */ +_c(15, R8Srgb, SRGB) /* VK_FORMAT_R8_SRGB */ +_c(22, RG8Srgb, SRGB) /* VK_FORMAT_R8G8_SRGB */ +_c(29, RGB8Srgb, SRGB) /* VK_FORMAT_R8G8B8_SRGB */ +_c(43, RGBA8Srgb, SRGB) /* VK_FORMAT_R8G8B8A8_SRGB */ +_c(13, R8UI, UINT) /* VK_FORMAT_R8_UINT */ +_c(20, RG8UI, UINT) /* VK_FORMAT_R8G8_UINT */ +_c(27, RGB8UI, UINT) /* VK_FORMAT_R8G8B8_UINT */ +_c(41, RGBA8UI, UINT) /* VK_FORMAT_R8G8B8A8_UINT */ +_c(14, R8I, SINT) /* VK_FORMAT_R8_SINT */ +_c(21, RG8I, SINT) /* VK_FORMAT_R8G8_SINT */ +_c(28, RGB8I, SINT) /* VK_FORMAT_R8G8B8_SINT */ +_c(42, RGBA8I, SINT) /* VK_FORMAT_R8G8B8A8_SINT */ +_c(70, R16Unorm, UNORM) /* VK_FORMAT_R16_UNORM */ +_c(77, RG16Unorm, UNORM) /* VK_FORMAT_R16G16_UNORM */ +_c(84, RGB16Unorm, UNORM) /* VK_FORMAT_R16G16B16_UNORM */ +_c(91, RGBA16Unorm, UNORM) /* VK_FORMAT_R16G16B16A16_UNORM */ +_c(71, R16Snorm, SNORM) /* VK_FORMAT_R16_SNORM */ +_c(78, RG16Snorm, SNORM) /* VK_FORMAT_R16G16_SNORM */ +_c(85, RGB16Snorm, SNORM) /* VK_FORMAT_R16G16B16_SNORM */ +_c(92, RGBA16Snorm, SNORM) /* VK_FORMAT_R16G16B16A16_SNORM */ +_c(74, R16UI, UINT) /* VK_FORMAT_R16_UINT */ +_c(81, RG16UI, UINT) /* VK_FORMAT_R16G16_UINT */ +_c(88, RGB16UI, UINT) /* VK_FORMAT_R16G16B16_UINT */ +_c(95, RGBA16UI, UINT) /* VK_FORMAT_R16G16B16A16_UINT */ +_c(75, R16I, SINT) /* VK_FORMAT_R16_SINT */ +_c(82, RG16I, SINT) /* VK_FORMAT_R16G16_SINT */ +_c(89, RGB16I, SINT) /* VK_FORMAT_R16G16B16_SINT */ +_c(96, RGBA16I, SINT) /* VK_FORMAT_R16G16B16A16_SINT */ +_c(98, R32UI, UINT) /* VK_FORMAT_R32_UINT */ +_c(101, RG32UI, UINT) /* VK_FORMAT_R32G32_UINT */ +_c(104, RGB32UI, UINT) /* VK_FORMAT_R32G32B32_UINT */ +_c(107, RGBA32UI, UINT) /* VK_FORMAT_R32G32B32A32_UINT */ +_c(99, R32I, SINT) /* VK_FORMAT_R32_SINT */ +_c(102, RG32I, SINT) /* VK_FORMAT_R32G32_SINT */ +_c(105, RGB32I, SINT) /* VK_FORMAT_R32G32B32_SINT */ +_c(108, RGBA32I, SINT) /* VK_FORMAT_R32G32B32A32_SINT */ +_c(76, R16F, SFLOAT) /* VK_FORMAT_R16_SFLOAT */ +_c(83, RG16F, SFLOAT) /* VK_FORMAT_R16G16_SFLOAT */ +_c(90, RGB16F, SFLOAT) /* VK_FORMAT_R16G16B16_SFLOAT */ +_c(97, RGBA16F, SFLOAT) /* VK_FORMAT_R16G16B16A16_SFLOAT */ +_c(100, R32F, SFLOAT) /* VK_FORMAT_R32_SFLOAT */ +_c(103, RG32F, SFLOAT) /* VK_FORMAT_R32G32_SFLOAT */ +_c(106, RGB32F, SFLOAT) /* VK_FORMAT_R32G32B32_SFLOAT */ +_c(109, RGBA32F, SFLOAT) /* VK_FORMAT_R32G32B32A32_SFLOAT */ +_c(124, Depth16Unorm, UNORM) /* VK_FORMAT_D16_UNORM */ +_c(125, Depth24Unorm, UNORM) /* VK_FORMAT_X8_D24_UNORM_PACK32 */ +_c(126, Depth32F, SFLOAT) /* VK_FORMAT_D32_SFLOAT */ +_c(127, Stencil8UI, UINT) /* VK_FORMAT_S8_UINT */ +_c(128, Depth16UnormStencil8UI, UINT) /* VK_FORMAT_D16_UNORM_S8_UINT */ +_c(129, Depth24UnormStencil8UI, UINT) /* VK_FORMAT_D24_UNORM_S8_UINT */ +_c(130, Depth32FStencil8UI, UINT) /* VK_FORMAT_D32_SFLOAT_S8_UINT */ +#endif diff --git a/src/MagnumPlugins/KtxImporter/formatMapping.py b/src/MagnumPlugins/KtxImporter/formatMapping.py new file mode 100644 index 000000000..f73ba4ba5 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/formatMapping.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021 Vladimír Vondruš +# 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"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import argparse +from collections import namedtuple +import itertools +import os +import re + +parser = argparse.ArgumentParser() +parser.add_argument('magnum_source') +args = parser.parse_args() + +magnum_dir = args.magnum_source +vulkan_header = os.path.join(magnum_dir, 'src/MagnumExternal/Vulkan/flextVk.h') + +vulkan_formats = {} + +with open(vulkan_header, encoding='utf-8') as f: + lines = f.readlines() + for line in lines: + # Get numeric VkFormat values so we can dereference them directly + # This also finds VK_FORMAT_FEATURE_* but that's no big deal since + # there are no formats that start with FEATURE_ + match = re.search('^\s+VK_FORMAT_(\w+) = (\d+),?$', line) + if match: + assert(not match.group(1) in vulkan_formats) + vulkan_formats[match.group(1)] = match.group(2) + +Format = namedtuple('Format', 'compressed magnum vulkan_name vulkan suffix') +formats = [] + +format_header = os.path.join(magnum_dir, 'src/Magnum/Vk/PixelFormat.h') + +with open(format_header, encoding='utf-8') as f: + lines = f.readlines() + for line in lines: + # Get mapping from VkFormat to Magnum::Vk::PixelFormat + # PixelFormat and Vk::PixelFormat names are identical + match = re.search('^\s+(Compressed)?(\w+) = VK_FORMAT_(\w+),?$', line) + if match: + compressed = match.group(1) != None + magnum_name = match.group(2) + vulkan_name = match.group(3) + assert(vulkan_name in vulkan_formats) + + suffix = re.search('\w+_([U|S](NORM|INT|FLOAT|RGB))\w*', vulkan_name) + assert suffix != None + assert suffix.group(1) != 'URGB' + + formats.append(Format(compressed, magnum_name, vulkan_name, vulkan_formats[vulkan_name], suffix.group(1))) + +if len(formats) != 135: + print('Unexpected number of formats') + +# https://docs.python.org/dev/library/itertools.html#itertools-recipes +def partition(pred, iterable): + t1, t2 = itertools.tee(iterable) + return itertools.filterfalse(pred, t1), filter(pred, t2) + +compressed = lambda f : f.compressed +formats, compressed_formats = partition(compressed, formats) + +# There's no PVRTC2 in CompressedPixelFormat +compressed_formats = [f for f in compressed_formats if not f.magnum.startswith('Pvrtc2')] + +header = '''/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* Autogenerated from formatMapping.py! Do not edit! */ + +''' + +with open('formatMapping.hpp', 'w', encoding='utf-8') as outfile: + outfile.write(header) + outfile.write('/* VkFormat, PixelFormat, Implementation::VkFormatSuffix */\n') + outfile.write('#ifdef _c\n') + for format in formats: + outfile.write('_c({}, {}, {}) /* VK_FORMAT_{} */\n'.format(format.vulkan , format.magnum, format.suffix, format.vulkan_name)) + outfile.write('#endif\n') + +with open('compressedFormatMapping.hpp', 'w', encoding='utf-8') as outfile: + outfile.write(header) + outfile.write('/* VkFormat, CompressedPixelFormat, Implementation::VkFormatSuffix */\n') + outfile.write('#ifdef _c\n') + for format in compressed_formats: + outfile.write('_c({}, {}, {}) /* VK_FORMAT_{} */\n'.format(format.vulkan , format.magnum, format.suffix, format.vulkan_name)) + outfile.write('#endif\n') diff --git a/src/MagnumPlugins/KtxImporter/importStaticPlugin.cpp b/src/MagnumPlugins/KtxImporter/importStaticPlugin.cpp new file mode 100644 index 000000000..0e6a31365 --- /dev/null +++ b/src/MagnumPlugins/KtxImporter/importStaticPlugin.cpp @@ -0,0 +1,36 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + 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"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "MagnumPlugins/KtxImporter/configure.h" + +#ifdef MAGNUM_KTXIMPORTER_BUILD_STATIC +#include + +static int magnumKtxImporterStaticImporter() { + CORRADE_PLUGIN_IMPORT(KtxImporter) + return 1; +} CORRADE_AUTOMATIC_INITIALIZER(magnumKtxImporterStaticImporter) +#endif