From b32b699510b40420f5531b7699104f29ce9778e4 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Wed, 11 Sep 2024 20:42:33 +0200 Subject: [PATCH 01/10] BasisImporter: add HDR support --- .../BasisImporter/BasisImporter.conf | 10 +- .../BasisImporter/BasisImporter.cpp | 52 +++- .../BasisImporter/BasisImporter.h | 34 ++- .../BasisImporter/Test/BasisImporterTest.cpp | 230 ++++++++++++++++-- .../BasisImporter/Test/CMakeLists.txt | 17 ++ .../BasisImporter/Test/configure.h.cmake | 1 + .../BasisImporter/Test/convert.sh | 8 + .../BasisImporter/Test/rgb-63x27.exr | Bin 0 -> 1477 bytes .../BasisImporter/Test/rgba-15x6.exr | Bin 0 -> 653 bytes .../BasisImporter/Test/rgba-31x13.exr | Bin 0 -> 1084 bytes .../BasisImporter/Test/rgba-63x27.exr | Bin 0 -> 1858 bytes .../BasisImporter/Test/rgbaf.basis | Bin 0 -> 1892 bytes .../BasisImporter/Test/rgbaf.ktx2 | Bin 0 -> 699 bytes .../BasisImporter/Test/rgbf.basis | Bin 0 -> 1892 bytes .../BasisImporter/Test/rgbf.ktx2 | Bin 0 -> 525 bytes 15 files changed, 323 insertions(+), 29 deletions(-) create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgb-63x27.exr create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgba-15x6.exr create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgba-31x13.exr create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgba-63x27.exr create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgbaf.basis create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgbaf.ktx2 create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgbf.basis create mode 100644 src/MagnumPlugins/BasisImporter/Test/rgbf.ktx2 diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.conf b/src/MagnumPlugins/BasisImporter/BasisImporter.conf index 06e0bd153..b3ed1ece8 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.conf +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.conf @@ -6,11 +6,15 @@ provides=BasisImporterBc1RGB provides=BasisImporterBc3RGBA provides=BasisImporterBc4R provides=BasisImporterBc5RG +provides=BasisImporterBc6hRGB provides=BasisImporterBc7RGBA provides=BasisImporterPvrtcRGB4bpp provides=BasisImporterPvrtcRGBA4bpp provides=BasisImporterAstc4x4RGBA +provides=BasisImporterAstc4x4RGBAF provides=BasisImporterRGBA8 +provides=BasisImporterRGB16F +provides=BasisImporterRGBA16F # [configuration_] [configuration] @@ -24,8 +28,8 @@ assumeYUp= # No format is specified by default and you have to choose one either by # changing this value or by loading the plugin under an alias. Should be one -# of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, -# PvrtcRGB4bpp, PvrtcRGBA4bpp, Astc4x4RGBA or RGBA8. If not set, falls back -# to RGBA8 with a warning. +# of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, +# Bc7RGBA, PvrtcRGB4bpp, PvrtcRGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, +# RGB16F or RGBA16F. If not set, falls back to RGBA8 or RGBA16F with a warning. format= # [configuration_] diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp index 70c23e249..077527d21 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp @@ -54,6 +54,10 @@ PixelFormat pixelFormat(BasisImporter::TargetFormat type, bool isSrgb) { switch(type) { case BasisImporter::TargetFormat::RGBA8: return isSrgb ? PixelFormat::RGBA8Srgb : PixelFormat::RGBA8Unorm; + case BasisImporter::TargetFormat::RGB16F: + return PixelFormat::RGB16F; + case BasisImporter::TargetFormat::RGBA16F: + return PixelFormat::RGBA16F; default: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } } @@ -74,6 +78,8 @@ CompressedPixelFormat compressedPixelFormat(BasisImporter::TargetFormat type, bo return CompressedPixelFormat::Bc4RUnorm; case BasisImporter::TargetFormat::Bc5RG: return CompressedPixelFormat::Bc5RGUnorm; + case BasisImporter::TargetFormat::Bc6hRGB: + return CompressedPixelFormat::Bc6hRGBUfloat; case BasisImporter::TargetFormat::Bc7RGBA: return isSrgb ? CompressedPixelFormat::Bc7RGBASrgb : CompressedPixelFormat::Bc7RGBAUnorm; case BasisImporter::TargetFormat::PvrtcRGB4bpp: @@ -82,6 +88,8 @@ CompressedPixelFormat compressedPixelFormat(BasisImporter::TargetFormat type, bo return isSrgb ? CompressedPixelFormat::PvrtcRGBA4bppSrgb : CompressedPixelFormat::PvrtcRGBA4bppUnorm; case BasisImporter::TargetFormat::Astc4x4RGBA: return isSrgb ? CompressedPixelFormat::Astc4x4RGBASrgb : CompressedPixelFormat::Astc4x4RGBAUnorm; + case BasisImporter::TargetFormat::Astc4x4RGBAF: + return CompressedPixelFormat::Astc4x4RGBAF; /** @todo use etc2/eacR/eacRG based on channel count? needs a bit from https://github.com/BinomialLLC/basis_universal/issues/66 */ case BasisImporter::TargetFormat::EacR: @@ -100,11 +108,13 @@ constexpr const char* FormatNames[]{ nullptr, nullptr, /* ATC formats */ "RGBA8", nullptr, nullptr, nullptr, /* RGB565 / BGR565 / RGBA4444 */ nullptr, nullptr, nullptr, - "EacR", "EacRG" + "EacR", "EacRG", + "Bc6hRGB", "Astc4x4RGBAF", + "RGB16F", "RGBA16F", }; /* Last element has to be on the same index as last enum value */ -static_assert(Containers::arraySize(FormatNames) - 1 == Int(BasisImporter::TargetFormat::EacRG), "bad string format mapping"); +static_assert(Containers::arraySize(FormatNames) - 1 == Int(BasisImporter::TargetFormat::RGBA16F), "bad string format mapping"); } @@ -518,7 +528,7 @@ template Containers::Optional> Bas targetFormat = configuration().value("format"); if(UnsignedInt(*targetFormat) == ~UnsignedInt{}) { Error{} << prefix << "invalid transcoding target format" << targetFormatStr << Debug::nospace - << ", expected to be one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA or RGBA8"; + << ", expected to be one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F"; return Containers::NullOpt; } } @@ -531,19 +541,36 @@ template Containers::Optional> Bas _state->yFlipNotPossibleWarningPrinted = false; } + #if BASISD_LIB_VERSION >= 150 + const bool isHdr = _state->compressionType == basist::basis_tex_format::cUASTC_HDR_4x4; + #else + constexpr bool isHdr = false; + #endif + /* If no target format was specified, print a warning and fall back to - RGBA8. Don't save it to _state->lastTargetFormat because then we - wouldn't be able to distinguish the case of explicit RGBA8 and no + RGBA. Don't save it to _state->lastTargetFormat because then we + wouldn't be able to distinguish the case of explicit RGBA and no format set */ if(!targetFormat) { if(!(flags() & ImporterFlag::Quiet) && !_state->noTranscodeFormatWarningPrinted) - Warning{} << prefix << "no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, PvrtcRGB4bpp, PvrtcRGBA4bpp, Astc4x4RGBA or RGBA8, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases."; - targetFormat = TargetFormat::RGBA8; + Warning{} << prefix << "no format to transcode to was specified, falling back to uncompressed" << (isHdr ? "RGBA16F" : "RGBA8") << Debug::nospace << "." + << "To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases."; + targetFormat = isHdr ? TargetFormat::RGBA16F : TargetFormat::RGBA8; _state->noTranscodeFormatWarningPrinted = true; } const auto format = basist::transcoder_texture_format(Int(*targetFormat)); - const bool isUncompressed = basist::basis_transcoder_format_is_uncompressed(format); + + #if BASISD_LIB_VERSION >= 150 + const bool wantHdr = basist::basis_transcoder_format_is_hdr(format); + + /* Basis doesn't support transcoding from LDR to HDR or vice versa */ + if(isHdr != wantHdr) { + Error{} << prefix << "Can't transcode" << (isHdr ? "HDR" : "LDR") << "image to" + << (wantHdr ? "HDR" : "LDR") << "format" << targetFormatStr; + return Containers::NullOpt; + } + #endif /* Some target formats may be unsupported, either because support wasn't compiled in or UASTC doesn't support a certain format. All of the @@ -610,6 +637,8 @@ template Containers::Optional> Bas _state->lastTranscodedImageId = id; } + const bool isUncompressed = basist::basis_transcoder_format_is_uncompressed(format); + const Vector3ui size{origWidth, origHeight, _state->numSlices}; UnsignedInt rowStride, outputRowsInPixels, outputSizeInBlocksOrPixels; if(isUncompressed) { @@ -705,10 +734,12 @@ template Containers::Optional> Bas break; case TargetFormat::Etc1RGB: case TargetFormat::Etc2RGBA: + case TargetFormat::Bc6hRGB: case TargetFormat::Bc7RGBA: case TargetFormat::PvrtcRGB4bpp: case TargetFormat::PvrtcRGBA4bpp: case TargetFormat::Astc4x4RGBA: + case TargetFormat::Astc4x4RGBAF: case TargetFormat::EacR: case TargetFormat::EacRG: if(!(flags() & ImporterFlag::Quiet) && !_state->yFlipNotPossibleWarningPrinted) @@ -717,7 +748,10 @@ template Containers::Optional> Bas flipped = false; break; /* We'd be in the isUncompressed branch above for this */ - case TargetFormat::RGBA8: CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + case TargetFormat::RGBA8: + case TargetFormat::RGB16F: + case TargetFormat::RGBA16F: + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ } if(flipped && !(flags() & ImporterFlag::Quiet) && size.y() % blockSize.y() != 0) diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.h b/src/MagnumPlugins/BasisImporter/BasisImporter.h index 7ef812a96..6dbed4b28 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.h +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.h @@ -72,8 +72,10 @@ You can use @ref BasisImageConverter to transcode images into this format. This plugin provides `BasisImporterEacR`, `BasisImporterEacRG`, `BasisImporterEtc1RGB`, `BasisImporterEtc2RGBA`, `BasisImporterBc1RGB`, `BasisImporterBc3RGBA`, `BasisImporterBc4R`, `BasisImporterBc5RG`, -`BasisImporterBc7RGBA`, `BasisImporterPvrtc1RGB4bpp`, -`BasisImporterPvrtc1RGBA4bpp`, `BasisImporterAstc4x4RGBA`, `BasisImporterRGBA8`. +`BasisImporterBc6hRGB`,, `BasisImporterBc7RGBA`, `BasisImporterPvrtc1RGB4bpp`, +`BasisImporterPvrtc1RGBA4bpp`, `BasisImporterAstc4x4RGBA`, +`BasisImporterAstc4x4RGBAF`, `BasisImporterRGBA8`, `BasisImporterRGB16F`, +`BasisImporterRGBA16F`. @m_class{m-block m-success} @@ -254,6 +256,10 @@ you may also use @ref setTargetFormat(). @snippet BasisImporter.cpp target-format-config +HDR images can only be transcoded to one of the HDR formats (`Bc6hRGB` / +`Astc4x4RGBAF` / `RGB16F` / `RGBA16F`). Likewise, LDR images can only be +transcoded to non-HDR formats. + There are many options and you should generally be striving for the highest-quality format available on a given platform. A detailed description of the choices can be found in the [Basis Universal Wiki](https://github.com/BinomialLLC/basis_universal/wiki/How-to-Deploy-ETC1S-Texture-Content-Using-Basis-Universal). @@ -389,6 +395,30 @@ class MAGNUM_BASISIMPORTER_EXPORT BasisImporter: public AbstractImporter { * @ref CompressedPixelFormat::EacRG11Unorm. */ EacRG = 21, + + /** + * BC6 RGB unsigned HDR. Loaded as + * @ref CompressedPixelFormat::Bc6hRGBUfloat. + */ + Bc6hRGB = 22, + + /** + * ASTC 4x4 RGBA HDR. The alpha channel is always set to opaque. + * Loaded as @ref CompressedPixelFormat::Astc4x4RGBAF. + */ + Astc4x4RGBAF = 23, + + /** + * Uncompressed half float RGB HDR. Loaded as + * @ref PixelFormat::RGB16F. + */ + RGB16F = 24, + + /** + * Uncompressed half float RGBA HDR. The alpha channel is always + * set to opaque. Loaded as @ref PixelFormat::RGBA16F. + */ + RGBA16F = 25, }; /** diff --git a/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp b/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp index be2e7a6e8..2bbe9a93a 100644 --- a/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp +++ b/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,8 @@ #include #endif +#include /* BASISD_LIB_VERSION */ + #include "configure.h" namespace Magnum { namespace Trade { namespace Test { namespace { @@ -61,6 +64,7 @@ struct BasisImporterTest: TestSuite::Tester { void fileTooShort(); void unconfigured(); + void unconfiguredHdr(); void invalidConfiguredFormat(); void unsupportedFormat(); void transcodingFailure(); @@ -72,8 +76,10 @@ struct BasisImporterTest: TestSuite::Tester { void rgbUncompressed(); void rgbUncompressedLinear(); + void rgbUncompressedHdr(); void rgbaUncompressed(); void rgbaUncompressedUastc(); + void rgbaUncompressedHdr(); void rgbaUncompressedMultipleImages(); void rgb(); @@ -178,6 +184,20 @@ const struct { {"quiet", ImporterFlag::Quiet, true} }; +const struct { + const char* name; + const bool requiresHdr; + const char* file; + const char* format; + const char* message; +} InvalidConfiguredFormatData[]{ + {"unknown", false, "rgba.basis", "Banana", "Trade::BasisImporter::image2D(): invalid transcoding target format Banana, expected to be one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F\n"}, + {"hdr to ldr", true, "rgbaf.basis", "RGBA8", "Trade::BasisImporter::image2D(): Can't transcode HDR image to LDR format RGBA8\n"}, + {"hdr to compressed ldr", true, "rgbaf.basis", "Bc3RGBA", "Trade::BasisImporter::image2D(): Can't transcode HDR image to LDR format Bc3RGBA\n"}, + {"ldr to hdr", true, "rgba.basis", "RGBA16F", "Trade::BasisImporter::image2D(): Can't transcode LDR image to HDR format RGBA16F\n"}, + {"ldr to compressed hdr", true, "rgba.basis", "Astc4x4RGBAF", "Trade::BasisImporter::image2D(): Can't transcode LDR image to HDR format Astc4x4RGBAF\n"}, +}; + #ifdef MAGNUM_BUILD_DEPRECATED const struct { const char* name; @@ -279,7 +299,15 @@ constexpr struct { {"rgb", "rgba", "rgb-linear", {63, 27}, "EacRG", CompressedPixelFormat::EacRG11Unorm, - CompressedPixelFormat::EacRG11Unorm} + CompressedPixelFormat::EacRG11Unorm}, + {"rgbf", "rgbaf", nullptr, {63, 27}, + "Bc6hRGB", + CompressedPixelFormat::Bc6hRGBUfloat, + CompressedPixelFormat::Bc6hRGBUfloat}, + {"rgbf", "rgbaf", nullptr, {63, 27}, + "Astc4x4RGBAF", + CompressedPixelFormat::Astc4x4RGBAF, + CompressedPixelFormat::Astc4x4RGBAF}, }; const struct { @@ -511,8 +539,12 @@ BasisImporterTest::BasisImporterTest() { addInstancedTests({&BasisImporterTest::unconfigured}, Containers::arraySize(QuietData)); - addTests({&BasisImporterTest::invalidConfiguredFormat, - &BasisImporterTest::unsupportedFormat, + addTests({&BasisImporterTest::unconfiguredHdr}); + + addInstancedTests({&BasisImporterTest::invalidConfiguredFormat}, + Containers::arraySize(InvalidConfiguredFormatData)); + + addTests({&BasisImporterTest::unsupportedFormat, &BasisImporterTest::transcodingFailure, &BasisImporterTest::nonBasisKtx}); @@ -521,12 +553,12 @@ BasisImporterTest::BasisImporterTest() { Containers::arraySize(TextureData)); #endif - addInstancedTests({&BasisImporterTest::rgbUncompressed}, - Containers::arraySize(FileTypeData)); - - addInstancedTests({&BasisImporterTest::rgbUncompressedLinear, + addInstancedTests({&BasisImporterTest::rgbUncompressed, + &BasisImporterTest::rgbUncompressedLinear, + &BasisImporterTest::rgbUncompressedHdr, &BasisImporterTest::rgbaUncompressed, - &BasisImporterTest::rgbaUncompressedUastc}, + &BasisImporterTest::rgbaUncompressedUastc, + &BasisImporterTest::rgbaUncompressedHdr}, Containers::arraySize(FileTypeData)); addTests({&BasisImporterTest::rgbaUncompressedMultipleImages}); @@ -579,11 +611,14 @@ BasisImporterTest::BasisImporterTest() { #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT _manager.setPluginDirectory({}); #endif - /* Load StbImageImporter from the build tree, if defined. Otherwise it's + /* Load optional plugins from the build tree, if defined. Otherwise they're static and already loaded. */ #ifdef STBIMAGEIMPORTER_PLUGIN_FILENAME CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(STBIMAGEIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); #endif + #ifdef OPENEXRIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(OPENEXRIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif /* Load the plugin directly from the build tree. Otherwise it's static and already loaded. */ #ifdef BASISIMPORTER_PLUGIN_FILENAME @@ -685,29 +720,76 @@ void BasisImporterTest::unconfigured() { if(data.quiet) CORRADE_COMPARE(out.str(), ""); else - CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, PvrtcRGB4bpp, PvrtcRGBA4bpp, Astc4x4RGBA or RGBA8, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); if(_manager.loadState("PngImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("PngImporter plugin not found, cannot test contents"); + /* Drop the alpha channel. Using a StridedArrayView also makes CompareImage + not complain about a format mismatch because PngImporter always imports + as RGB8Unorm. */ CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels()), Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.png"), /* There are moderately significant compression artifacts */ (DebugTools::CompareImageToFile{_manager, 58.334f, 6.622f})); } +void BasisImporterTest::unconfiguredHdr() { + #if BASISD_LIB_VERSION < 150 + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + + Containers::Pointer importer = _manager.instantiate("BasisImporter"); + CORRADE_VERIFY(importer->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgbf.basis"))); + + std::ostringstream out; + Containers::Optional image; + { + Warning redirectWarning{&out}; + image = importer->image2D(0); + /* It should warn just once, not spam every time */ + image = importer->image2D(0); + } + CORRADE_VERIFY(image); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA16F); + CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); + + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA16F. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("OpenExrImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("OpenExrImporter plugin not found, cannot test contents"); + + /* mutablePixels() so we can use the convenient slice syntax. Const + overload of rgb() returns by value which makes it not work. */ + CORRADE_COMPARE_WITH(image->mutablePixels().slice(&Color4h::rgb), + Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.exr"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 0.052f, 0.004f})); +} + void BasisImporterTest::invalidConfiguredFormat() { + auto&& data = InvalidConfiguredFormatData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + #if BASISD_LIB_VERSION < 150 + if(data.requiresHdr) + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + Containers::Pointer importer = _manager.instantiate("BasisImporter"); - CORRADE_VERIFY(importer->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgb.basis"))); + CORRADE_VERIFY(importer->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, data.file))); std::ostringstream out; Error redirectError{&out}; - importer->configuration().setValue("format", "Banana"); + importer->configuration().setValue("format", data.format); CORRADE_VERIFY(!importer->image2D(0)); - CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): invalid transcoding target format Banana, expected to be one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA or RGBA8\n"); + CORRADE_COMPARE(out.str(), data.message); } void BasisImporterTest::unsupportedFormat() { @@ -887,6 +969,43 @@ void BasisImporterTest::rgbUncompressedLinear() { (DebugTools::CompareImageToFile{_manager, 61.0f, 6.321f})); } +void BasisImporterTest::rgbUncompressedHdr() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + #if BASISD_LIB_VERSION < 150 + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGB16F"); + CORRADE_COMPARE(importer->configuration().value("format"), "RGB16F"); + /* See the comment above this variable for more details. The + rgbf.{basis,ktx2} are both encoded with Y up in convert.sh but the KTX + files are missing the KTXorientation metadata, causing a warning. */ + if(data.hasMissingOrientationMetadata) + importer->configuration().setValue("assumeYUp", true); + CORRADE_VERIFY(importer->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, + "rgbf"_s + data.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->flags(), ImageFlags2D{}); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGB16F); + CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("OpenExrImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("OpenExrImporter plugin not found, cannot test contents"); + + CORRADE_COMPARE_WITH(*image, + Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.exr"), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, 0.052f, 0.004f})); +} + void BasisImporterTest::rgbaUncompressed() { auto&& data = FileTypeData[testCaseInstanceId()]; setTestCaseDescription(data.name); @@ -952,6 +1071,69 @@ void BasisImporterTest::rgbaUncompressedUastc() { (DebugTools::CompareImageToFile{_manager, 4.75f, 0.576f})); } +void BasisImporterTest::rgbaUncompressedHdr() { + auto&& data = FileTypeData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + #if BASISD_LIB_VERSION < 150 + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA16F"); + /* See the comment above this variable for more details. The + rgbaf.{basis,ktx2} are both encoded with Y up in convert.sh but the KTX + files are missing the KTXorientation metadata, causing a warning. */ + if(data.hasMissingOrientationMetadata) + importer->configuration().setValue("assumeYUp", true); + CORRADE_VERIFY(importer->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, + "rgbaf"_s + data.extension))); + CORRADE_COMPARE(importer->image2DCount(), 1); + + Containers::Optional image = importer->image2D(0); + CORRADE_VERIFY(image); + CORRADE_COMPARE(image->flags(), ImageFlags2D{}); + CORRADE_VERIFY(!image->isCompressed()); + CORRADE_COMPARE(image->format(), PixelFormat::RGBA16F); + CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); + + /* mutablePixels() so we can use the convenient slice syntax. Const + overloads of a()/rgb() return by value which makes it not work. */ + const auto pixels = image->mutablePixels(); + + /* For HDR images alpha is always transcoded to 1. Can't use a + StridedArrayView with broadcasted size since CompareImage only + accepts StridedArrayView for the actual image, not the expected. */ + using namespace Math::Literals; + Containers::Array ones{DirectInit, size_t(image->size().product()), 1.0_h}; + + CORRADE_COMPARE_WITH(pixels.slice(&Color4h::a), + (ImageView2D{PixelStorage{}.setAlignment(1), PixelFormat::R16F, image->size(), ones}), + /* No errors, always exactly 1.0 */ + (DebugTools::CompareImage{0.0f, 0.0f})); + + if(_manager.loadState("AnyImageImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("AnyImageImporter plugin not found, cannot test contents"); + if(_manager.loadState("OpenExrImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("OpenExrImporter plugin not found, cannot test contents"); + + Containers::Pointer imageImporter = _manager.instantiate("OpenExrImporter"); + + /* Drop alpha channel to only test RGB. Not using CompareImageToFile to + avoid overwriting the 4-channel expected .exr with a 3-channel + actual image when --save-diagnostic is specified. */ + imageImporter->configuration().setValue("a", ""); + + CORRADE_VERIFY(imageImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.exr"))); + const auto expected = imageImporter->image2D(0); + CORRADE_VERIFY(expected); + + imageImporter->configuration().setValue("a", "A"); + + CORRADE_COMPARE_WITH(pixels.slice(&Color4h::rgb), *expected, + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImage{0.637f, 0.009f})); +} + void BasisImporterTest::rgbaUncompressedMultipleImages() { Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); CORRADE_VERIFY(importer->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, @@ -1014,6 +1196,13 @@ void BasisImporterTest::rgb() { auto& formatData = FormatData[testCaseInstanceId()]; setTestCaseDescription(formatData.suffix); + #if BASISD_LIB_VERSION < 150 + /* Iff there is no linear version, this is an HDR format */ + const bool isHdr = formatData.fileBaseLinear == nullptr; + if(isHdr) + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + #if defined(BASISD_SUPPORT_BC7) && !BASISD_SUPPORT_BC7 /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ @@ -1055,6 +1244,13 @@ void BasisImporterTest::rgba() { auto& formatData = FormatData[testCaseInstanceId()]; setTestCaseDescription(formatData.suffix); + #if BASISD_LIB_VERSION < 150 + /* Iff there is no linear version, this is an HDR format */ + const bool isHdr = formatData.fileBaseLinear == nullptr; + if(isHdr) + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + #if defined(BASISD_SUPPORT_BC7) && !BASISD_SUPPORT_BC7 /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ @@ -1098,6 +1294,10 @@ void BasisImporterTest::linear() { /* Test linear formats, sRGB was tested in rgb() */ + /* HDR test files have no separate "linear" images */ + if(!formatData.fileBaseLinear) + CORRADE_SKIP("This format has no linear version."); + #if defined(BASISD_SUPPORT_BC7) && !BASISD_SUPPORT_BC7 /* BC7 is YUUGE and thus defined out on Emscripten. Skip the test if that's the case. This assumes -DBASISD_SUPPORT_*=0 is supplied globally. */ @@ -1890,7 +2090,7 @@ void BasisImporterTest::importMultipleFormats() { CORRADE_VERIFY(image); CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); - CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, PvrtcRGB4bpp, PvrtcRGBA4bpp, Astc4x4RGBA or RGBA8, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); /* Second time without a format change it shouldn't repeat the same warning */ @@ -1963,7 +2163,7 @@ void BasisImporterTest::importMultipleFormats() { CORRADE_VERIFY(image); CORRADE_COMPARE(image->format(), PixelFormat::RGBA8Srgb); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); - CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc7RGBA, PvrtcRGB4bpp, PvrtcRGBA4bpp, Astc4x4RGBA or RGBA8, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); + CORRADE_COMPARE(out.str(), "Trade::BasisImporter::image2D(): no format to transcode to was specified, falling back to uncompressed RGBA8. To get rid of this warning, either explicitly set the format option to one of Etc1RGB, Etc2RGBA, EacR, EacRG, Bc1RGB, Bc3RGBA, Bc4R, Bc5RG, Bc6hRGB, Bc7RGBA, Pvrtc1RGB4bpp, Pvrtc1RGBA4bpp, Astc4x4RGBA, Astc4x4RGBAF, RGBA8, RGB16F or RGBA16F, or load the plugin via one of its BasisImporterEtc1RGB, ... aliases.\n"); } } diff --git a/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt b/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt index eaa91989b..cf93d5dcb 100644 --- a/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt @@ -47,6 +47,9 @@ if(NOT MAGNUM_BASISIMPORTER_BUILD_STATIC) if(MAGNUM_WITH_STBIMAGEIMPORTER) set(STBIMAGEIMPORTER_PLUGIN_FILENAME $) endif() + if(MAGNUM_WITH_OPENEXRIMPORTER) + set(OPENEXRIMPORTER_PLUGIN_FILENAME $) + endif() if(MAGNUM_WITH_BCDECIMAGECONVERTER) set(BCDECIMAGECONVERTER_PLUGIN_FILENAME $) endif() @@ -114,6 +117,14 @@ corrade_add_test(BasisImporterTest BasisImporterTest.cpp rgba-27x27-slice2.png ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/KtxImporter/Test/2d-rgba.ktx2) target_include_directories(BasisImporterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) +# The test needs BASISD_LIB_VERSION from basisu_transcoder.h +target_include_directories(BasisImporterTest SYSTEM PRIVATE ${BasisUniversalTranscoder_INCLUDE_DIR}) +# BasisUniversalTranscoder_DEFINITIONS isn't exported by find_package, so we +# need to grab it from the target +get_target_property(BasisUniversalTranscoder_DEFINITIONS BasisUniversal::Transcoder + INTERFACE_COMPILE_DEFINITIONS) +set_property(TARGET BasisImporterTest APPEND PROPERTY + COMPILE_DEFINITIONS ${BasisUniversalTranscoder_DEFINITIONS}) if(MAGNUM_BASISIMPORTER_BUILD_STATIC) target_link_libraries(BasisImporterTest PRIVATE BasisImporter) if(Magnum_AnyImageImporter_FOUND) @@ -122,6 +133,9 @@ if(MAGNUM_BASISIMPORTER_BUILD_STATIC) if(MAGNUM_WITH_STBIMAGEIMPORTER) target_link_libraries(BasisImporterTest PRIVATE StbImageImporter) endif() + if(MAGNUM_WITH_OPENEXRIMPORTER) + target_link_libraries(BasisImporterTest PRIVATE OpenExrImporter) + endif() if(MAGNUM_WITH_BCDECIMAGECONVERTER) target_link_libraries(BasisImporterTest PRIVATE BcDecImageConverter) endif() @@ -134,6 +148,9 @@ else() if(MAGNUM_WITH_STBIMAGEIMPORTER) add_dependencies(BasisImporterTest StbImageImporter) endif() + if(MAGNUM_WITH_OPENEXRIMPORTER) + add_dependencies(BasisImporterTest OpenExrImporter) + endif() if(MAGNUM_WITH_BCDECIMAGECONVERTER) add_dependencies(BasisImporterTest BcDecImageConverter) endif() diff --git a/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake b/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake index 479dde994..a0f42a7f8 100644 --- a/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake +++ b/src/MagnumPlugins/BasisImporter/Test/configure.h.cmake @@ -27,6 +27,7 @@ #cmakedefine BASISIMPORTER_PLUGIN_FILENAME "${BASISIMPORTER_PLUGIN_FILENAME}" #cmakedefine STBIMAGEIMPORTER_PLUGIN_FILENAME "${STBIMAGEIMPORTER_PLUGIN_FILENAME}" +#cmakedefine OPENEXRIMPORTER_PLUGIN_FILENAME "${OPENEXRIMPORTER_PLUGIN_FILENAME}" #cmakedefine BCDECIMAGECONVERTER_PLUGIN_FILENAME "${BCDECIMAGECONVERTER_PLUGIN_FILENAME}" #cmakedefine ETCDECIMAGECONVERTER_PLUGIN_FILENAME "${ETCDECIMAGECONVERTER_PLUGIN_FILENAME}" #define KTXIMPORTER_TEST_DIR "${KTXIMPORTER_TEST_DIR}" diff --git a/src/MagnumPlugins/BasisImporter/Test/convert.sh b/src/MagnumPlugins/BasisImporter/Test/convert.sh index b43fbd7df..316b31a7f 100755 --- a/src/MagnumPlugins/BasisImporter/Test/convert.sh +++ b/src/MagnumPlugins/BasisImporter/Test/convert.sh @@ -88,3 +88,11 @@ basisu rgb-64x32.png -output_file rgb-linear-pow2.basis -y_flip -linear basisu rgb-64x32.png -output_file rgb-linear-pow2.ktx2 -y_flip -linear -ktx2 basisu rgba-64x32.png -output_file rgba-pow2.basis -force_alpha -y_flip basisu rgba-64x32.png -output_file rgba-pow2.ktx2 -force_alpha -y_flip -ktx2 + +# HDR images +# Requires v1_50_0_2 tag +# This version switched the default from -basis to -ktx2 +basisu rgb-63x27.exr -output_file rgbf.basis -y_flip -basis +basisu rgb-63x27.exr -output_file rgbf.ktx2 -y_flip +basisu rgba-63x27.exr -output_file rgbaf.basis -y_flip -basis +basisu rgba-63x27.exr -output_file rgbaf.ktx2 -y_flip diff --git a/src/MagnumPlugins/BasisImporter/Test/rgb-63x27.exr b/src/MagnumPlugins/BasisImporter/Test/rgb-63x27.exr new file mode 100644 index 0000000000000000000000000000000000000000..dee15bb5b6c8bfa1654aa1417872321d3d80f87a GIT binary patch literal 1477 zcmXTZH)LdDU|>kjNX*Mi%_#;lax#lc7|elUP7I7d5(Gdr2)kp51Yw9UBYIa`F>P7+Am-HP{y?7p11=L5z1!%_{*KP-c|I zzyWgykVaM#o|#gTfo24QC)gqehLg+)T@}EPtcZI%%{D{CQKIdrmteG`&N;6m`wuy7 z32+p+%yGRU;#YB|$%HxgD#aIX-(2qYcx52-ty2p#`KBxoJLu*pD6&{nspE#r1rbd? z7r}#v=H5(xUcGaw;bHEZ%)SfQ=6)d-3w5r}=V84o|{u z>K10*|NhnU*pqm(x1SbW4Ym9IO@hs(?(4H1spmH_1I7E#zdg`U_HSBNx z8!q@{9Gwwp@Y7q~SVG|E{m*ZG10y9{>?ik}yl>Z-tfepawyVnSXWd~(*@ZR#4o(c@ z*zmdN{uYpPXdg z)$%2WR~|cbs(8;HS^fN@r*D7!6gF+kAAV=Ii^~<1-FYQj|IL+(NI#*`+}5+<{)9lW ztv1$&t~D_*J4u9vt_}ZcYksoEC3Q!i;^O%I`=>h#xct4FunI+k`GX^m-U`+IBVkE>aCo_L2B z^vsdwIh4>+C+f3LTd_{KK6~mB-+Q9rm2K1i=Og(`tF0%8PpZPoEy(CaR`T|Uop!B!bTgFkQO;EL9g4z)g#ak`S4nfN~4tlY2&t?t`v=PX8 zvn9a)dvaQ=J%7js!Gru8L`AP0I(8^bm*wIqrwt$YHw1Cd{TNYuztk;KVB?MF57Y4jG_|H1og(RQAgrXO`H^XKNSW%%y($3Mz8{lJ5U$Y$r3gN%FG+RsSH1Q=MT z^lqPY?6&9QwKZ!h1dm+X)AM=S$3wx&OP&|k2ymQRSP}T)>iG;0F7ZpnhXRFU^si*k z;5lCH{7{0ktK5xAGsbk@f*%fh7yn}D5wD%RP|CH&?x^)V`M$*uD&pgR7|1RBp;-Ep zg{$j(>%Fa)dOBohD{MHvd)>a$Ot&L_z0YqwZoM))Y?k20eRrtTkdDz?FE%2V> Y@4{cKf0!p)%-e6e{>Xji;9`DL043;*sQ>@~ literal 0 HcmV?d00001 diff --git a/src/MagnumPlugins/BasisImporter/Test/rgba-15x6.exr b/src/MagnumPlugins/BasisImporter/Test/rgba-15x6.exr new file mode 100644 index 0000000000000000000000000000000000000000..e00fa3f0d41e4e43a43e3f8063046c0f3c422218 GIT binary patch literal 653 zcmXTZH)LdDU|>kjNX*Mi%_#;lax#lc7(9Vujtq=I5(Gdr2s>ejxMPR}VTdp!=jRp_ zr4|=w=I5cXK}IsCB$gzGXXd5kmop^gR~Th72mnQ4fDg!E1>%&<;)0ySN`mTgGV@aX zi&9dH7!WM5CkiqvQga-O3sRFyf)YzI^BK}|@)JuKSilxF*cT@krKaXVjCW4WD*+l% zW|YRj0dohCMphD@nNpI0W&}ep*dmAyBOtqCPVXf{@52rf#|jrJ^v$)}Hre^$QOA}= z3$;4p5AgBv<>g7WUJIG5J~2dErsU#eH>>Z%>=WDqb_HRhMi^p0xdi(E{9H8*wYKcBZ!c*?tT q$kjNX*Mi%_#;lax#lc7(9Vujtq=I5(Gdr2s>ejxMPR}VTdp!=jRp_ zr4|=w=I5cXK}IsCB$gzGXXd5kmop^gR~Th72mnQ4Kn}>@0pgU*;)0ySN`mTgGV@aX zi&9dH7!WM5CkiqvQga-O3sRFyf)YzI^BK}|@)JuKSilxF*cT@krKaXVjCW4WD*+l% zW|YRj0dohCMphD@nNpI0W&}ep*dmCIhrlqcn6q`7e}=H5$g!!nR%rxQ3)n1G(g?G? z;l+BinIlR!N&l^9bB1St&aAGfe{P=5$(a?FC9NH`BJPM-(gv?9S5|Q}RRjtOay@eG z?ojH8SR%qw`uuF(uKUTcf{!Ph*>nF~%D%s8Y4P7SHcYZ@u+d$8a-o0HCH2MAvUbhN z-`bXY=%SmhX;ebQ#`rM)-R50wO+8Nb1v~m8wsOxjdG;o9dH0f@n$nW3%O9?K73WjB zF0$^-rQ04dw|+a6=1;roQR2OIy{p?szd1GRj~sZ5Sg+m;%???RyI4(jDy317P?KyQN{7HF7^YjU?1;u%ljqk6Qo_JgE`}2w73i~R$Y@`d1IbUp2re)3BU}}FnFICYf`g4x{C($FdtPw)o_rxScixf2~-y1`m?^^Qu0#ZyimesjKg>Gv(Gw|Ph$S}E(zssDCI?VaFj8D|${9+ndR zd@axU%nG;d?mM%^cTX*IGQM*7+OxHHUp~9NFjrxBwzxOfNBylc-|w50Rlf1!|I6P$ zN=WzJc)0fN;axKY%ldY1uJBwdfPwhoRn33 z|BS`#Klf`spZxz|_mb>)t9-A^=udSM^o+iKc8~k>XDKZ8zPF+d`Iz-3r*4{>{pd|x h-Q(K@`_9jra3y_wiqYnCFLbrGn*|lu_=!7@{AZ-4P_I5fK1)O=|KB*v5QV%0PI6}`Xy!`J z7T2d$V=tK`t9c};ApwQmRiaOMG}Hd;C-gp;$>2-DaSjD>{L!R2!csebr;g-!j=dwu&o# zV!|rdN&abv-|Bf>foD-&KV*Ut6E==VIM#;62)b%tFSEI^C*NrMZ5nQ0RR3VVRdg`W zx?i`&EnV8=S`Xe86gw`6;cKBN7{Cs)uB~eFS|<1ruTT+)lb}-X#+$vb22 zGoSs*K9WTysiUz?OwJI=9y6^g2DDDIRRmdFHu0)4lgqART5Cg7_OSePTeJmg-Ol>N z`KZZ~9EC9pV*l|9M&!ui=(i-l0?+Cts|Y;1*`XE?qm>;jDS}ClWRHXdOUgJjLc+&K zuwTk~KD8GR5^cM_tq?!^zJ8Yj4%0NnHr8RHob1~}3vXNAZiKzweS1bEG9KJynzYc% z9@V#(On4`5tad0@ggECRqag)CTWhlh_+8&ox9SwRytHjT+>R*A%}S&)+c?n2wl}Q2 zsluaW+8+E~FTGKZtB9bWNl2K1Yu|>)`>c;-978r@qLm zk)Dq<3B9R=b3F{$FZHp|266Dxg;Tqwu4x7WwC^5{3lkUf)TF1$z(YsiIofZRIi)xD zX?&x9hxh1hn?!Xpr%Ne9IoKMP{)h2P#-KnQq}x4KUk|%>eL?9 zM*Z1VbC49fQX|tFu+2sN=&rmDhj_u{KPvc6>UdNh>VpbMdS<2Ojqbp`9MtHQ^d-E4 zk5b1&uD;gRADFp>a#l*kD>Z~$;+^@+b_#x`#XVz=W7cXZoTHz+j8pI*sy43wz1YnY zCcmiQN2}x0GEv|@8Ta=wf3?xsn9qkhmMr(zz0)d+ml(joHT7fh1|f~tN)d)PH%8Be zXE?C$SX2kh%Xit)3XU3egvQA$@>l(I`8y`tHctz<4f72=cpi9G1%6zzm6CDw*Gcb# zhEaF#`U$}&BuD8mz2}F9KZUz%(y~MI>fv)PUX>S%&d|DAv%7G^G78aO_lKiMy%H+= zpvDLd#ja3~_)RaNv8t;zU?JLE*uS*JXJb2EKh+UxM9T~8$!Krau$ma`O@3Bpn#XA7 z5mJ3F*j%l5kIqNBfSJmEv>sUeo}=bqB$P=vU4s==83<*C#{N&(;$cP$kMQXSsuq*) zp@)D@KRmQWsidL`wKRqVa2^_QySXDL*wH@+#b%^0(N=0SH{i=*uri>w8+wE%^DGj& z6?~+ZVFzGtASrcJmdQT|>d~G_GV7B?Q3E)yR1sX+?K-h?s;JhzGvtO6?h>83Y*E8B#zJKvS2#4@`Q?$~uXm z(&1&<(%;+j!!h}w1>HhDHVfP7fBP#tx?>?*EQT3<~ew-M#zn-KtgS{J^A_nynpS0SrPbJw(9j zD-&<5S#7f0m*Ee?1cnDd_oTLU&FEs7)ZDqmQm`9cKOz2~{}3?4=DUr%HiHeV`9Sxg z_#YHr-F;P644p12hg|Z~2=iYvw{|eGIQ;(q_Wo2iBGltI9~giC8Z_*=X99ts;LMq{ z=A-)`WZ#h_IfhlM($dbH0mVOk`GHBFzkS;j7Re|CO5cnOwB|c$C|xUi^XBTZW3N~l znVACulh%K^x2Y-;n6-gm0x*BkoBx`*zK^Gjq4P@Z{hNQ@0po|pe5n7B^1sBQdGVn@ z07`+N{7O$g$iJ<9Oce}5l7cU@yn*E(Ccn9lrOKhuy76UNJ}|$%Ol|6#)yA-id9f<6 zJOrlKtp^vW3EEfu=LMA{*z}{CkLrI|dE>6_Y<|@SQU2iK1MM%=>v}YWk*PBbR34M6 zA6-2pzPV}`W}nTOcaupNlmB(grY#Zij7$v-20R=LK>w~hxJb+7e?1V!|K|@(g2*?D xC~z?_9bm-N4@ytS=41E2DVqOr@qzwN*6Vq(h>`!K+Y<#*xc_mfhxr$mJOI2GQRV;u literal 0 HcmV?d00001 diff --git a/src/MagnumPlugins/BasisImporter/Test/rgbaf.ktx2 b/src/MagnumPlugins/BasisImporter/Test/rgbaf.ktx2 new file mode 100644 index 0000000000000000000000000000000000000000..099c152e74607e2698593f6102137375bed79d32 GIT binary patch literal 699 zcmZ4O9TK5nWU!l;ON#5l>Dktd3=9nRKr9W!FaQz*VJ0BX0Ad{=o&v-wK#UG{Kn0jV zia~%KLV@&v026}-!*WJO24-dmA41kQAb1V-@<0yIS>;8UC8+m?9pS^p=%%5<{iK%d(}vx95kyj%Mj&n#@qp>7k-D zv12L&LxWRNw{D|Ky%9IV`LFc_|1;}Z`WhM;1UNldlo~snlDPjnDlsU$dw2KlyLYQr z1tz`JZ0!gOU=UjAAyS!mW6f%l-M$Qe7$z`02uymN+SWCri(yi8=Mqc7?mz!$*nGEf z*JiNk?yIU|=yXvz
6nz^-uiN)df|F`$2x}^c_{@0*k&pi_e1O;c#?EZHoNseLF zsS(d&SDg%p4GywEoMzO;wSM3==wDGuQX= zlreN(sl9*m&pU}l^WsB+ppU77K}b^YWtMk%b015UL!ou!%e4G7p!-vsx@NU8Y+_!l zy7k~9H9`A||Ge(n&gNHb7;J#Dg?e3&rZ6&fhH=#}%s!hl?PH82?P za4-O!yz<~8EtCKCKp6j@zfnYii-G9?qbX2xvR==NMU4C>-JU3j8Y&bpaZZ_Wf?1)# zIq>X*M}BMz8Zs^3H53>hzZh+BvAK;^!bkh8LH~w)9*4F}zH8=l*s3I)OO`3+%(}E^ zVT-Pu!cmD6touCoa&{gq{gQEO$C|h^7fjioFrLkv+{mtM`ogk_fz8cMi5;-;n`rZwsPYS@ z9^F1-{TrC{GO?v&ULQl4PBF-SAU__+pX89*$)|wi9%9YIO9Nw5VA9SnTQDktd3=9nRKr9W!FaQz*VJ0BX0Ad{=o&v-wK#UG{Km|fU z8bE*@LV@&v026}-!*WJO24-dmA41kQAb1V-@<0yIS>;8UC8We`!vu5<~olycZLZa-#=~o5)sb$$R<_i?#55szQlzy3S@eyC{3Jb z^EWW*WnxRmygr67onjz69>|{LklM+o0JO`JQN>>1+y8I>zx@C3AIL9md}6QtpC1V2 z|JwuY7H#j!%5Vt39o3q(b5&r{*BzU-M8q>Pa5S?tFg67y?fkN3Q%oeI0H=qDQey{D zyU+NgC}TVBa22sCqQoXKObS>(8%Ern_F278YQM;J0CHr<*p$Q5zv-iziS v2Yu6gi$t#Q Date: Fri, 27 Sep 2024 13:26:06 +0200 Subject: [PATCH 02/10] BasisImporter: handle output formats that are not a multiple of 4 bytes --- .../BasisImporter/BasisImporter.cpp | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp index 077527d21..f8869b01a 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -638,20 +639,24 @@ template Containers::Optional> Bas } const bool isUncompressed = basist::basis_transcoder_format_is_uncompressed(format); + const UnsignedInt formatSize = basis_get_bytes_per_block_or_pixel(format); const Vector3ui size{origWidth, origHeight, _state->numSlices}; - UnsignedInt rowStride, outputRowsInPixels, outputSizeInBlocksOrPixels; + UnsignedInt rowStrideInBlocksOrPixels, outputRowsInPixels, outputSizeInBlocksOrPixels; if(isUncompressed) { - rowStride = size.x(); + rowStrideInBlocksOrPixels = size.x(); outputRowsInPixels = size.y(); outputSizeInBlocksOrPixels = size.x()*size.y(); } else { - rowStride = 0; /* left up to Basis to calculate */ + rowStrideInBlocksOrPixels = 0; /* left up to Basis to calculate */ outputRowsInPixels = 0; /* not used for compressed data */ outputSizeInBlocksOrPixels = totalBlocks; + /* All compressed formats have a size that matches the default 4-byte + alignment of CompressedPixelStorage */ + CORRADE_INTERNAL_ASSERT(formatSize % 4 == 0); } - const UnsignedInt sliceSize = basis_get_bytes_per_block_or_pixel(format)*outputSizeInBlocksOrPixels; + const UnsignedInt sliceSize = formatSize*outputSizeInBlocksOrPixels; const UnsignedInt dataSize = sliceSize*size.z(); Containers::Array dest{DefaultInit, dataSize}; @@ -669,7 +674,7 @@ template Containers::Optional> Bas #if BASISD_SUPPORT_KTX2 if(_state->ktx2Transcoder) { const UnsignedInt currentLayer = id + l; - if(!_state->ktx2Transcoder->transcode_image_level(level, currentLayer, f, dest.data() + offset, outputSizeInBlocksOrPixels, format, 0, rowStride, outputRowsInPixels)) { + if(!_state->ktx2Transcoder->transcode_image_level(level, currentLayer, f, dest.data() + offset, outputSizeInBlocksOrPixels, format, 0, rowStrideInBlocksOrPixels, outputRowsInPixels)) { Error{} << prefix << "transcoding failed"; return Containers::NullOpt; } @@ -677,7 +682,7 @@ template Containers::Optional> Bas #endif { const UnsignedInt currentId = id + (l*numFaces + f); - if(!_state->basisTranscoder->transcode_image_level(_state->in.data(), _state->in.size(), currentId, level, dest.data() + offset, outputSizeInBlocksOrPixels, format, 0, rowStride, nullptr, outputRowsInPixels)) { + if(!_state->basisTranscoder->transcode_image_level(_state->in.data(), _state->in.size(), currentId, level, dest.data() + offset, outputSizeInBlocksOrPixels, format, 0, rowStrideInBlocksOrPixels, nullptr, outputRowsInPixels)) { Error{} << prefix << "transcoding failed"; return Containers::NullOpt; } @@ -686,7 +691,30 @@ template Containers::Optional> Bas } if(isUncompressed) { - Trade::ImageData out{pixelFormat(*targetFormat, _state->isSrgb), Math::Vector::pad(Vector3i{size}), Utility::move(dest), ImageFlag(UnsignedShort(_state->imageFlags))}; + const Magnum::PixelFormat outFormat = pixelFormat(*targetFormat, _state->isSrgb); + const Math::Vector outSize = Math::Vector::pad(Vector3i{size}); + /* All (compressed or uncompressed) formats have a block/pixel size + that's a multiple of 4, matching the PixelStorage default alignment + of 4. *Except* for RGB16F. Basis transcoding functions only take row + stride in pixels, not bytes, so we need to manually copy to a + correctly aligned image. */ + const UnsignedInt rowWidthInBytes = formatSize*size.x(); + if(rowWidthInBytes % 4 != 0) { + const UnsignedInt rowStrideInBytes = (rowWidthInBytes + 3)/4*4; + const UnsignedInt alignedDataSize = rowStrideInBytes*size.y()*size.z(); + Containers::Array alignedDest{DefaultInit, alignedDataSize}; + + PixelStorage unalignedStorage; + unalignedStorage.setAlignment(1); + const BasicImageView src{unalignedStorage, outFormat, outSize, dest}; + const BasicMutableImageView dst{outFormat, outSize, alignedDest}; + + Utility::copy(src.pixels(), dst.pixels()); + + dest = Utility::move(alignedDest); + } + + Trade::ImageData out{outFormat, outSize, Utility::move(dest), ImageFlag(UnsignedShort(_state->imageFlags))}; /* Flip if needed */ if(!_state->isYFlipped) From 77179b2e0d407998a53f68af6ab58fbc3f38c3d5 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Wed, 11 Sep 2024 20:00:29 +0200 Subject: [PATCH 03/10] BasisImageConverter: add HDR support --- .../BasisImageConverter.conf | 5 + .../BasisImageConverter.cpp | 170 ++++++++++----- .../BasisImageConverter/BasisImageConverter.h | 23 ++- .../Test/BasisImageConverterTest.cpp | 195 ++++++++++++++---- .../BasisImageConverter/Test/CMakeLists.txt | 9 + .../Test/configure.h.cmake | 1 + 6 files changed, 305 insertions(+), 98 deletions(-) diff --git a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf index 88cf2ed09..22ee0fe9c 100644 --- a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf +++ b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.conf @@ -73,6 +73,11 @@ rdo_uastc_max_allowed_rms_increase_ratio=10.0 rdo_uastc_skip_block_rms_threshold=8.0 rdo_uastc_favor_simpler_modes_in_rdo_mode=true +# HDR options +# Quality level from 0 (fastest) to 4 (highest quality) +hdr_quality_level=1 +hdr_favor_astc=false + # KTX2 options ktx2_uastc_supercompression=true ktx2_zstd_supercompression_level=6 diff --git a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp index 4cb877b97..173b56061 100644 --- a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp +++ b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp @@ -56,23 +56,90 @@ using namespace Containers::Literals; namespace { +template void copySlice(const ImageView3D& image3D, UnsignedInt slice, Containers::StridedArrayView2D>& dst, bool yFlip, bool hasCustomSwizzle) { + const UnsignedInt channelCount = pixelFormatChannelCount(image3D.format()); + + if(yFlip) + dst = dst.template flipped<0>(); + + /* basis image is always RGBA, fill in alpha if necessary */ + if(channelCount == 4) { + const auto src = image3D.pixels>()[slice]; + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = src[y][x]; + + } else if(channelCount == 3) { + const auto src = image3D.pixels>()[slice]; + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::Vector3{src[y][x]}; /* Alpha implicitly opaque */ + + } else if(channelCount == 2) { + const auto src = image3D.pixels>()[slice]; + /* If the user didn't specify a custom swizzle, assume they want + the two channels compressed in separate slices, R in RGB and G + in Alpha. This significantly improves quality. */ + if(!hasCustomSwizzle) + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::gather<'r', 'r', 'r', 'g'>(src[y][x]); + else + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::Vector3::pad(src[y][x]); /* Alpha implicitly opaque */ + + } else if(channelCount == 1) { + const auto src = image3D.pixels>()[slice]; + /* If the user didn't specify a custom swizzle, assume they want + a gray-scale image. Alpha is always implicitly opaque. */ + if(!hasCustomSwizzle) + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::gather<'r', 'r', 'r'>(src[y][x]); + else + for(std::size_t y = 0; y != src.size()[0]; ++y) + for(std::size_t x = 0; x != src.size()[1]; ++x) + dst[y][x] = Math::Vector3::pad(src[y][x]); /* Alpha implicitly opaque */ + + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + template Containers::Optional> convertLevelsToData(Containers::ArrayView> imageLevels, const Utility::ConfigurationGroup& configuration, ImageConverterFlags flags, BasisImageConverter::Format fileFormat) { /* Check input */ const PixelFormat pixelFormat = imageLevels.front().format(); bool isSrgb; + #if BASISU_LIB_VERSION >= 150 + bool isHdr; + #endif switch(pixelFormat) { case PixelFormat::RGBA8Unorm: case PixelFormat::RGB8Unorm: case PixelFormat::RG8Unorm: case PixelFormat::R8Unorm: isSrgb = false; + #if BASISU_LIB_VERSION >= 150 + isHdr = false; + #endif break; case PixelFormat::RGBA8Srgb: case PixelFormat::RGB8Srgb: case PixelFormat::RG8Srgb: case PixelFormat::R8Srgb: isSrgb = true; + #if BASISU_LIB_VERSION >= 150 + isHdr = false; + #endif break; + #if BASISU_LIB_VERSION >= 150 + case PixelFormat::R32F: + case PixelFormat::RG32F: + case PixelFormat::RGB32F: + case PixelFormat::RGBA32F: + isSrgb = false; + isHdr = true; + break; + #endif default: Error{} << "Trade::BasisImageConverter::convertToData(): unsupported format" << pixelFormat; return {}; @@ -260,6 +327,15 @@ template Containers::Optional> c } #endif + /* HDR */ + #if BASISU_LIB_VERSION >= 150 + params.m_hdr = isHdr; + + params.m_uastc_hdr_options.init(); + params.m_uastc_hdr_options.set_quality_level(configuration.value("hdr_quality_level")); + PARAM_CONFIG(hdr_favor_astc, bool); + #endif + /* Set various fields in the Basis file header */ PARAM_CONFIG(userdata0, int); PARAM_CONFIG(userdata1, int); @@ -279,17 +355,29 @@ template Containers::Optional> c #else params.m_write_output_basis_files = false; #endif + /* One image per slice. The base mip is in m_source_images, mip 1 and higher go into m_source_mipmap_images. */ const UnsignedInt numImages = Vector3i::pad(baseSize, 1).z(); - params.m_source_images.resize(numImages); - if(numMipmaps > 1) { - params.m_source_mipmap_images.resize(numImages); - for(auto& slice: params.m_source_mipmap_images) - slice.resize(numMipmaps - 1); + #if BASISU_LIB_VERSION >= 150 + if(isHdr) { + params.m_source_images_hdr.resize(numImages); + if(numMipmaps > 1) { + params.m_source_mipmap_images_hdr.resize(numImages); + for(auto& slice: params.m_source_mipmap_images_hdr) + slice.resize(numMipmaps - 1); + } + } else + #endif + { + params.m_source_images.resize(numImages); + if(numMipmaps > 1) { + params.m_source_mipmap_images.resize(numImages); + for(auto& slice: params.m_source_mipmap_images) + slice.resize(numMipmaps - 1); + } } - const UnsignedInt channelCount = pixelFormatChannelCount(pixelFormat); for(UnsignedInt level = 0; level != numMipmaps; ++level) { const auto mipSize = Math::max(baseSize >> level, 1)*mipMask + baseSize*(Math::Vector{1} - mipMask); const auto& image = imageLevels[level]; @@ -306,57 +394,27 @@ template Containers::Optional> c /* Copy image data into the basis image. There is no way to construct a basis image from existing data as it is based on basisu::vector, moreover we need to tightly pack it and flip Y. */ - basisu::image& basisImage = level > 0 ? params.m_source_mipmap_images[slice][level - 1] : params.m_source_images[slice]; - basisImage.resize(mipSize[0], mipSize[1]); - auto dst = Containers::arrayCast(Containers::StridedArrayView2D({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(mipSize[1]), std::size_t(mipSize[0])})); - /* Y-flip the view to make the following loops simpler. basisu doesn't - apply m_y_flip to user-supplied mipmaps, so only do this for the - base image: + /* basisu doesn't apply m_y_flip to user-supplied mipmaps, so only + do this for the base image: https://github.com/BinomialLLC/basis_universal/issues/257 */ - if(!params.m_y_flip || level == 0) - dst = dst.flipped<0>(); - - /* basis image is always RGBA, fill in alpha if necessary */ - if(channelCount == 4) { - const auto src = image3D.pixels>()[slice]; - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = src[y][x]; - - } else if(channelCount == 3) { - const auto src = image3D.pixels>()[slice]; - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Vector3ub{src[y][x]}; /* Alpha implicitly 255 */ - - } else if(channelCount == 2) { - const auto src = image3D.pixels>()[slice]; - /* If the user didn't specify a custom swizzle, assume they want - the two channels compressed in separate slices, R in RGB and G - in Alpha. This significantly improves quality. */ - if(!swizzle) - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Math::gather<'r', 'r', 'r', 'g'>(src[y][x]); - else - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Vector3ub::pad(src[y][x]); /* Alpha implicitly 255 */ - - } else if(channelCount == 1) { - const auto src = image3D.pixels>()[slice]; - /* If the user didn't specify a custom swizzle, assume they want - a gray-scale image. Alpha is always implicitly 255. */ - if(!swizzle) - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Math::gather<'r', 'r', 'r'>(src[y][x]); - else - for(std::size_t y = 0; y != src.size()[0]; ++y) - for(std::size_t x = 0; x != src.size()[1]; ++x) - dst[y][x] = Vector3ub::pad(src[y][x]); - - } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + const bool yFlip = !params.m_y_flip || level == 0; + const bool hasCustomSwizzle = !swizzle.isEmpty(); + #if BASISU_LIB_VERSION >= 150 + if(isHdr) { + basisu::imagef& basisImage = level > 0 ? params.m_source_mipmap_images_hdr[slice][level - 1] : params.m_source_images_hdr[slice]; + basisImage.resize(mipSize[0], mipSize[1]); + auto dst = Containers::arrayCast(Containers::StridedArrayView2D({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(mipSize[1]), std::size_t(mipSize[0])})); + + copySlice(image3D, slice, dst, yFlip, hasCustomSwizzle); + } else + #endif + { + basisu::image& basisImage = level > 0 ? params.m_source_mipmap_images[slice][level - 1] : params.m_source_images[slice]; + basisImage.resize(mipSize[0], mipSize[1]); + auto dst = Containers::arrayCast(Containers::StridedArrayView2D({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(mipSize[1]), std::size_t(mipSize[0])})); + + copySlice(image3D, slice, dst, yFlip, hasCustomSwizzle); + } } } diff --git a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h index 162168f6e..9e8e30e45 100644 --- a/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h +++ b/src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h @@ -154,9 +154,16 @@ Accepts 2D, 2D array, cube map and cube map array images, recognizing The @ref PixelFormat::R8Unorm, @relativeref{PixelFormat,R8Srgb}, @relativeref{PixelFormat,RG8Unorm}, @relativeref{PixelFormat,RG8Srgb}, @relativeref{PixelFormat,RGB8Unorm}, @relativeref{PixelFormat,RGB8Srgb}, -@relativeref{PixelFormat,RGBA8Unorm} and @relativeref{PixelFormat,RGBA8Srgb} +@relativeref{PixelFormat,RGBA8Unorm}, @relativeref{PixelFormat,RGBA8Srgb}, +@relativeref{PixelFormat,R32F} and @relativeref{PixelFormat,RG32F}, +@relativeref{PixelFormat,RGB32F} and @relativeref{PixelFormat,RGBA32F} formats are supported. +HDR floating point formats require at least version 1.50 of Basis. + +The alpha channel of @relativeref{PixelFormat,RGBA32F} will be dropped and +transcoded to 1 because Basis UASTC HDR doesn't support alpha channels. + Even though the KTX container format supports 1D, 1D array and 3D images, Basis Universal doesn't. In particular, if a 2D image with @ref ImageFlag2D::Array is passed, the conversion will fail as it's not possible to represent 1D array @@ -184,12 +191,14 @@ If no user-specified channel mapping is supplied through the @cb{.ini} swizzle @ce @ref Trade-BasisImageConverter-configuration "configuration option", the converter swizzles 1- and 2-channel formats before compression as follows: -- 1-channel formats (@ref PixelFormat::R8Unorm / @ref PixelFormat::R8Srgb) - are remapped as RRR, producing an opaque gray-scale image -- 2-channel formats (@ref PixelFormat::RG8Unorm / @ref PixelFormat::RG8Srgb) - are remapped as RRRG, ie. G becomes the alpha channel. This significantly - improves compressed image quality because RGB and alpha get separate slices - instead of the two channels being compressed into a single slice. +- 1-channel formats (@ref PixelFormat::R8Unorm / @ref PixelFormat::R8Srgb / + @ref PixelFormat::R32F) are remapped as RRR, producing an opaque gray-scale + image +- 2-channel formats (@ref PixelFormat::RG8Unorm / @ref PixelFormat::RG8Srgb / + @ref PixelFormat::RG32F) are remapped as RRRG, ie. G becomes the alpha + channel. This significantly improves compressed image quality because RGB + and alpha get separate slices instead of the two channels being compressed + into a single slice. Setting the @cb{.ini} swizzle @ce option to any value disables this behavior. To keep the original channel order, set @cb{.ini} swizzle=rgba @ce. diff --git a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp index a16a17021..f921b3b5e 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp +++ b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -77,6 +78,7 @@ struct BasisImageConverterTest: TestSuite::Tester { void convert2DRgb(); void convert2DRgba(); void convert2DMipmaps(); + void convert2DMipmapsHdr(); void convertUastcPatchAwaySrgb(); @@ -123,6 +125,14 @@ constexpr struct { {"2D array", {{4, 5, 3}, {2, 2, 1}}, "expected size Vector(2, 2, 3) for level 1 but got Vector(2, 2, 1)"} }; +constexpr struct { + const char* name; + const bool isHdr; +} MipGenData[]{ + {"", false}, + {"hdr", true} +}; + enum TransferFunction: std::size_t { Linear, Srgb @@ -186,6 +196,9 @@ const struct { {"Srgb UASTC Basis, perceptual=false", "BasisImageConverter", PixelFormat::RGBA8Srgb, "false", true, true, PixelFormat::RGBA8Unorm, "Trade::BasisImageConverter::convertToData(): patching away an incorrect sRGB flag in the output Basis file\n"}, + {"HDR Basis", "BasisImageConverter", + PixelFormat::RGBA32F, "", false, true, PixelFormat::RGBA16F, + "Trade::BasisImageConverter::convertToData(): patching away an incorrect sRGB flag in the output Basis file\n"}, /* The KTX container doesn't suffer from this problem */ {"Unorm UASTC KTX2", "BasisKtxImageConverter", @@ -194,6 +207,9 @@ const struct { {"Srgb UASTC KTX2, perceptual=false", "BasisKtxImageConverter", PixelFormat::RGBA8Srgb, "false", true, true, PixelFormat::RGBA8Unorm, ""}, + {"HDR KTX2", "BasisKtxImageConverter", + PixelFormat::RGBA32F, "", false, true, PixelFormat::RGBA16F, + ""}, }; const struct { @@ -303,11 +319,12 @@ BasisImageConverterTest::BasisImageConverterTest() { Containers::arraySize(LevelWrongSizeData)); addTests({&BasisImageConverterTest::processError, + &BasisImageConverterTest::configPerceptual}); - &BasisImageConverterTest::configPerceptual, - &BasisImageConverterTest::configMipGen, + addInstancedTests({&BasisImageConverterTest::configMipGen}, + Containers::arraySize(MipGenData)); - &BasisImageConverterTest::convert1DArray}); + addTests({&BasisImageConverterTest::convert1DArray}); addInstancedTests({&BasisImageConverterTest::convert2DR, &BasisImageConverterTest::convert2DRg, @@ -321,6 +338,8 @@ BasisImageConverterTest::BasisImageConverterTest() { addInstancedTests({&BasisImageConverterTest::convert2DMipmaps}, Containers::arraySize(QuietData)); + addTests({&BasisImageConverterTest::convert2DMipmapsHdr}); + addInstancedTests({&BasisImageConverterTest::convert2DArray}, Containers::arraySize(Convert2DArrayData)); @@ -357,19 +376,22 @@ BasisImageConverterTest::BasisImageConverterTest() { #ifndef CORRADE_PLUGINMANAGER_NO_DYNAMIC_PLUGIN_SUPPORT _manager.setPluginDirectory({}); #endif - /* Load StbImageImporter from the build tree, if defined. Otherwise it's + /* Load optional plugins from the build tree, if defined. Otherwise they're static and already loaded. */ #ifdef STBIMAGEIMPORTER_PLUGIN_FILENAME CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(STBIMAGEIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); #endif + #ifdef BASISIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(BASISIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif + #ifdef OPENEXRIMPORTER_PLUGIN_FILENAME + CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(OPENEXRIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); + #endif /* Load the plugin directly from the build tree. Otherwise it's static and already loaded. */ #ifdef BASISIMAGECONVERTER_PLUGIN_FILENAME CORRADE_INTERNAL_ASSERT_OUTPUT(_converterManager.load(BASISIMAGECONVERTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); #endif - #ifdef BASISIMPORTER_PLUGIN_FILENAME - CORRADE_INTERNAL_ASSERT_OUTPUT(_manager.load(BASISIMPORTER_PLUGIN_FILENAME) & PluginManager::LoadState::Loaded); - #endif /* Create the output directory if it doesn't exist yet */ CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Path::make(BASISIMAGECONVERTER_TEST_OUTPUT_DIR)); @@ -381,8 +403,8 @@ void BasisImageConverterTest::wrongFormat() { const char data[8]{}; std::ostringstream out; Error redirectError{&out}; - CORRADE_VERIFY(!converter->convertToData(ImageView2D{PixelFormat::RG32F, {1, 1}, data})); - CORRADE_COMPARE(out.str(), "Trade::BasisImageConverter::convertToData(): unsupported format PixelFormat::RG32F\n"); + CORRADE_VERIFY(!converter->convertToData(ImageView2D{PixelFormat::R32I, {1, 1}, data})); + CORRADE_COMPARE(out.str(), "Trade::BasisImageConverter::convertToData(): unsupported format PixelFormat::R32I\n"); } void BasisImageConverterTest::unknownOutputFormatData() { @@ -553,9 +575,18 @@ void BasisImageConverterTest::configPerceptual() { } void BasisImageConverterTest::configMipGen() { - const char bytes[16*16*4]{}; - ImageView2D originalLevel0{PixelFormat::RGBA8Unorm, Vector2i{16}, bytes}; - ImageView2D originalLevel1{PixelFormat::RGBA8Unorm, Vector2i{8}, bytes}; + auto&& data = MipGenData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + #if BASISU_LIB_VERSION < 150 + if(isPixelFormatFloatingPoint(data.format)) + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + + const PixelFormat format = data.isHdr ? PixelFormat::RGBA32F : PixelFormat::RGBA8Unorm; + Containers::Array bytes{ValueInit, 16*16*pixelFormatSize(format)}; + ImageView2D originalLevel0{format, Vector2i{16}, bytes}; + ImageView2D originalLevel1{format, Vector2i{8}, bytes}; Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); /* Empty by default */ @@ -571,7 +602,8 @@ void BasisImageConverterTest::configMipGen() { if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("BasisImporter plugin not found, cannot test"); - Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + const Containers::StringView importerName = data.isHdr ? "BasisImporterRGBA16F"_s : "BasisImporterRGBA8"_s; + Containers::Pointer importer = _manager.instantiate(importerName); /* Empty mip_gen config means to use the level count to determine if mip levels should be generated */ @@ -589,7 +621,7 @@ Image copyImageWithSkip(const BasicImageView& image, Mat format = image.format(); /* Width includes row alignment to 4 bytes */ const UnsignedInt formatSize = pixelFormatSize(format); - const UnsignedInt widthWithSkip = ((size[0] + skip[0])*DestinationType::Size + 3)/formatSize*formatSize; + const UnsignedInt widthWithSkip = ((size[0] + skip[0])*sizeof(DestinationType) + 3)/formatSize*formatSize; const UnsignedInt dataSize = widthWithSkip*(size + skip).product()/(size[0] + skip[0]); Image imageWithSkip{PixelStorage{}.setSkip(Vector3i::pad(skip)), format, size, Containers::Array{ValueInit, dataSize}, image.flags()}; @@ -789,6 +821,11 @@ void BasisImageConverterTest::convertUastcPatchAwaySrgb() { auto&& data = ConvertUastcPatchAwaySrgbData[testCaseInstanceId()]; setTestCaseDescription(data.name); + #if BASISU_LIB_VERSION < 150 + if(isPixelFormatFloatingPoint(data.format)) + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + Containers::Pointer converter = _converterManager.instantiate(data.pluginName); /* Yes, the damn thing prints output to stdout without any possibility to redirect anywhere. But we need the verbose output enabled in many cases @@ -797,20 +834,22 @@ void BasisImageConverterTest::convertUastcPatchAwaySrgb() { converter->configuration().setValue("uastc", data.uastc); converter->configuration().setValue("perceptual", data.perceptual); - const char imageData[4*4*4]{}; + Containers::Array imageData{ValueInit, pixelFormatSize(data.format)*4*4}; std::ostringstream out; Containers::Optional> compressedData; { Debug redirectOutput{&out}; - compressedData = converter->convertToData(ImageView2D{data.format, {4, 4}, Containers::arrayView(imageData)}); + compressedData = converter->convertToData(ImageView2D{data.format, {4, 4}, imageData}); CORRADE_VERIFY(compressedData); } if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("BasisImporter plugin not found, cannot test"); - Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA8"); + const Containers::StringView importerName = isPixelFormatFloatingPoint(data.format) + ? "BasisImporterRGBA16F"_s : "BasisImporterRGBA8"_s; + Containers::Pointer importer = _manager.instantiate(importerName); CORRADE_VERIFY(importer->openData(*compressedData)); Containers::Optional image = importer->image2D(0); CORRADE_VERIFY(image); @@ -831,11 +870,12 @@ void BasisImageConverterTest::convert2DMipmaps() { struct Level { const char* file; Containers::Optional imageWithSkip; - Containers::Optional result; + float maxThreshold; + float meanThreshold; } levels[3] { - {"rgba-63x27.png", {}, {}}, - {"rgba-31x13.png", {}, {}}, - {"rgba-15x6.png", {}, {}} + {"rgba-63x27.png", {}, 97.25f, 7.882f}, + {"rgba-31x13.png", {}, 81.0f, 14.33f}, + {"rgba-15x6.png", {}, 76.25f, 24.5f} }; for(Level& level: levels) { @@ -878,22 +918,107 @@ void BasisImageConverterTest::convert2DMipmaps() { for(std::size_t i = 0; i != Containers::arraySize(levels); ++i) { CORRADE_ITERATION("level" << i); - levels[i].result = importer->image2D(0, i); - CORRADE_VERIFY(levels[i].result); + const auto result = importer->image2D(0, i); + CORRADE_VERIFY(result); + CORRADE_COMPARE_WITH(*result, + Utility::Path::join(BASISIMPORTER_TEST_DIR, levels[i].file), + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImageToFile{_manager, levels[i].maxThreshold, levels[i].meanThreshold})); } +} - CORRADE_COMPARE_WITH(*levels[0].result, - Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgba-63x27.png"), - /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 97.25f, 7.882f})); - CORRADE_COMPARE_WITH(*levels[1].result, - Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgba-31x13.png"), - /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 81.0f, 14.33f})); - CORRADE_COMPARE_WITH(*levels[2].result, - Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgba-15x6.png"), - /* There are moderately significant compression artifacts */ - (DebugTools::CompareImageToFile{_manager, 76.25f, 24.5f})); +void BasisImageConverterTest::convert2DMipmapsHdr() { + #if BASISU_LIB_VERSION < 150 + CORRADE_SKIP("Current version of Basis doesn't support HDR."); + #endif + + if(_manager.loadState("OpenExrImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("OpenExrImporter plugin not found, cannot test contents"); + + Containers::Pointer imageImporter = _manager.instantiate("OpenExrImporter"); + + /* BasisImageConverter only reads 32-bit floats, but the test files are + half float images. Let openexr do the conversion for us. */ + imageImporter->configuration().setValue("forceChannelType", "FLOAT"); + + struct Level { + const char* file; + Containers::Optional imageWithSkip; + float maxThreshold; + float meanThreshold; + } levels[3] { + {"rgba-63x27.exr", {}, 0.491f, 0.009f}, + {"rgba-31x13.exr", {}, 0.339f, 0.018f}, + {"rgba-15x6.exr", {}, 0.562f, 0.029f} + }; + + for(Level& level: levels) { + CORRADE_ITERATION(level.file); + CORRADE_VERIFY(imageImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, level.file))); + Containers::Optional originalImage = imageImporter->image2D(0); + CORRADE_VERIFY(originalImage); + /* Use the original images and add a skip to ensure the converter reads + the image data properly */ + level.imageWithSkip = copyImageWithSkip(ImageView2D(*originalImage), {7, 8}); + } + + /* Revert back to loading the actual format. BasisImporter can't transcode + to 32-bit float, which is fine since the test files are half float as + well. */ + imageImporter->configuration().setValue("forceChannelType", ""); + + Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); + + /* HDR is encoded using UASTC HDR, but shouldn't require uastc to be + explicitly enabled */ + CORRADE_COMPARE(converter->configuration().value("uastc"), false); + + Containers::Optional> compressedData = converter->convertToData({*levels[0].imageWithSkip, *levels[1].imageWithSkip, *levels[2].imageWithSkip}); + CORRADE_VERIFY(compressedData); + + if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) + CORRADE_SKIP("BasisImporter plugin not found, cannot test"); + + Containers::Pointer importer = _manager.instantiate("BasisImporterRGBA16F"); + CORRADE_VERIFY(importer->openData(*compressedData)); + CORRADE_COMPARE(importer->image2DCount(), 1); + CORRADE_COMPARE(importer->image2DLevelCount(0), Containers::arraySize(levels)); + + for(std::size_t i = 0; i != Containers::arraySize(levels); ++i) { + CORRADE_ITERATION("level" << i); + auto result = importer->image2D(0, i); + CORRADE_VERIFY(result); + + /* mutablePixels() so we can use the convenient slice syntax. Const + overloads of a()/rgb() return by value which makes it not work. */ + const auto pixels = result->mutablePixels(); + + /* For HDR images alpha is always transcoded to 1. Can't use a + StridedArrayView with broadcasted size since CompareImage only + accepts StridedArrayView for the actual image, not the expected. */ + using namespace Math::Literals; + Containers::Array ones{DirectInit, size_t(result->size().product()), 1.0_h}; + + CORRADE_COMPARE_WITH(pixels.slice(&Color4h::a), + (ImageView2D{PixelStorage{}.setAlignment(1), PixelFormat::R16F, result->size(), ones}), + /* No errors, always exactly 1.0 */ + (DebugTools::CompareImage{0.0f, 0.0f})); + + /* Drop alpha channel to only test RGB. Not using CompareImageToFile to + avoid overwriting the 4-channel expected .exr with a 3-channel + actual image when --save-diagnostic is specified. */ + imageImporter->configuration().setValue("a", ""); + + CORRADE_VERIFY(imageImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, levels[i].file))); + const auto expected = imageImporter->image2D(0); + CORRADE_VERIFY(expected); + + imageImporter->configuration().setValue("a", "A"); + + CORRADE_COMPARE_WITH(pixels.slice(&Color4h::rgb), *expected, + /* There are moderately significant compression artifacts */ + (DebugTools::CompareImage{levels[i].maxThreshold, levels[i].meanThreshold})); + } } ImageView2D imageViewSlice(const ImageView3D& image, Int slice) { diff --git a/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt b/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt index d0fffcba8..7bddf270e 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt @@ -74,6 +74,9 @@ if(NOT MAGNUM_BASISIMAGECONVERTER_BUILD_STATIC) if(MAGNUM_WITH_STBIMAGEIMPORTER) set(STBIMAGEIMPORTER_PLUGIN_FILENAME $) endif() + if(MAGNUM_WITH_OPENEXRIMPORTER) + set(OPENEXRIMPORTER_PLUGIN_FILENAME $) + endif() endif() # On Mac OpenCL can be found but the runtime implementation could fail, listing @@ -118,6 +121,9 @@ if(MAGNUM_BASISIMAGECONVERTER_BUILD_STATIC) if(MAGNUM_WITH_STBIMAGEIMPORTER) target_link_libraries(BasisImageConverterTest PRIVATE StbImageImporter) endif() + if(MAGNUM_WITH_OPENEXRIMPORTER) + target_link_libraries(BasisImageConverterTest PRIVATE OpenExrImporter) + endif() else() # So the plugins get properly built when building the test add_dependencies(BasisImageConverterTest BasisImageConverter) @@ -127,6 +133,9 @@ else() if(MAGNUM_WITH_STBIMAGEIMPORTER) add_dependencies(BasisImageConverterTest StbImageImporter) endif() + if(MAGNUM_WITH_OPENEXRIMPORTER) + add_dependencies(BasisImageConverterTest OpenExrImporter) + endif() endif() if(CORRADE_BUILD_STATIC AND NOT MAGNUM_BASISIMAGECONVERTER_BUILD_STATIC) # CMake < 3.4 does this implicitly, but 3.4+ not anymore (see CMP0065). diff --git a/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake b/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake index 3a7925485..2b4979307 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake +++ b/src/MagnumPlugins/BasisImageConverter/Test/configure.h.cmake @@ -28,6 +28,7 @@ #cmakedefine BASISIMAGECONVERTER_PLUGIN_FILENAME "${BASISIMAGECONVERTER_PLUGIN_FILENAME}" #cmakedefine BASISIMPORTER_PLUGIN_FILENAME "${BASISIMPORTER_PLUGIN_FILENAME}" #cmakedefine STBIMAGEIMPORTER_PLUGIN_FILENAME "${STBIMAGEIMPORTER_PLUGIN_FILENAME}" +#cmakedefine OPENEXRIMPORTER_PLUGIN_FILENAME "${OPENEXRIMPORTER_PLUGIN_FILENAME}" #cmakedefine OpenCL_FOUND #cmakedefine _BASISIMAGECONVERTER_EXPECT_OPENCL_FRAMEWORK_FAILURE #define BASISIMPORTER_TEST_DIR "${BASISIMPORTER_TEST_DIR}" From 4ab691a8c0f049e3da0b53be842e030b2ef3b9da Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Tue, 24 Sep 2024 21:54:18 +0200 Subject: [PATCH 04/10] BasisImageConverter: list all files used in the test --- .../Test/BasisImageConverterTest.cpp | 16 ++++++++-------- .../BasisImageConverter/Test/CMakeLists.txt | 8 +++++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp index f921b3b5e..f3d1871ad 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp +++ b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp @@ -127,10 +127,10 @@ constexpr struct { constexpr struct { const char* name; - const bool isHdr; + const PixelFormat format; } MipGenData[]{ - {"", false}, - {"hdr", true} + {"", PixelFormat::RGBA8Unorm}, + {"hdr", PixelFormat::RGBA32F} }; enum TransferFunction: std::size_t { @@ -583,10 +583,9 @@ void BasisImageConverterTest::configMipGen() { CORRADE_SKIP("Current version of Basis doesn't support HDR."); #endif - const PixelFormat format = data.isHdr ? PixelFormat::RGBA32F : PixelFormat::RGBA8Unorm; - Containers::Array bytes{ValueInit, 16*16*pixelFormatSize(format)}; - ImageView2D originalLevel0{format, Vector2i{16}, bytes}; - ImageView2D originalLevel1{format, Vector2i{8}, bytes}; + Containers::Array bytes{ValueInit, pixelFormatSize(data.format)*16*16}; + ImageView2D originalLevel0{data.format, Vector2i{16}, bytes}; + ImageView2D originalLevel1{data.format, Vector2i{8}, bytes}; Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); /* Empty by default */ @@ -602,7 +601,8 @@ void BasisImageConverterTest::configMipGen() { if(_manager.loadState("BasisImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("BasisImporter plugin not found, cannot test"); - const Containers::StringView importerName = data.isHdr ? "BasisImporterRGBA16F"_s : "BasisImporterRGBA8"_s; + const Containers::StringView importerName = isPixelFormatFloatingPoint(data.format) + ? "BasisImporterRGBA16F"_s : "BasisImporterRGBA8"_s; Containers::Pointer importer = _manager.instantiate(importerName); /* Empty mip_gen config means to use the level count to determine if mip diff --git a/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt b/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt index 7bddf270e..a2b5be93e 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/BasisImageConverter/Test/CMakeLists.txt @@ -100,7 +100,13 @@ corrade_add_test(BasisImageConverterTest BasisImageConverterTest.cpp Threads::Threads FILES ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgb-63x27.png - ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-63x27.png) + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-27x27.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-63x27.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-31x13.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-15x6.png + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-63x27.exr + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-31x13.exr + ${PROJECT_SOURCE_DIR}/src/MagnumPlugins/BasisImporter/Test/rgba-15x6.exr) target_include_directories(BasisImageConverterTest PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$) # The test needs BASISU_LIB_VERSION from basisu_comp.h target_include_directories(BasisImageConverterTest SYSTEM PRIVATE ${BasisUniversalEncoder_INCLUDE_DIR}) From c0515081066e034be3905bf35348fff7bcad5d65 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Sat, 28 Sep 2024 01:02:58 +0200 Subject: [PATCH 05/10] GltfSceneConverter: adapt to BasisImageConverter adding HDR support R32F is now a valid format and leads to an unexpected successful image conversion --- .../GltfSceneConverter/Test/GltfSceneConverterTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MagnumPlugins/GltfSceneConverter/Test/GltfSceneConverterTest.cpp b/src/MagnumPlugins/GltfSceneConverter/Test/GltfSceneConverterTest.cpp index 078b64599..d97b993a6 100644 --- a/src/MagnumPlugins/GltfSceneConverter/Test/GltfSceneConverterTest.cpp +++ b/src/MagnumPlugins/GltfSceneConverter/Test/GltfSceneConverterTest.cpp @@ -679,8 +679,8 @@ const struct { /* Also tests that an extension isn't accidentally added even after a failure */ {"conversion to file failed", "BasisKtxImageConverter", ".gltf", - ImageData3D{PixelFormat::R32F, {1, 1, 1}, DataFlags{}, "abc", ImageFlag3D::Array}, - "Trade::BasisImageConverter::convertToData(): unsupported format PixelFormat::R32F\n" + ImageData3D{PixelFormat::R32I, {1, 1, 1}, DataFlags{}, "abc", ImageFlag3D::Array}, + "Trade::BasisImageConverter::convertToData(): unsupported format PixelFormat::R32I\n" "Trade::GltfSceneConverter::add(): can't convert an image file\n"}, /* Not testing failed conversion to data as that's the same code path as in the 2D case and the same failure return as the file check above */ From 683146e42e6a7340821fadfcbc3d72bf0418b443 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Sat, 28 Sep 2024 20:10:55 +0200 Subject: [PATCH 06/10] BasisImporter: list all files used in the test --- src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt b/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt index cf93d5dcb..9b5d051c5 100644 --- a/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt +++ b/src/MagnumPlugins/BasisImporter/Test/CMakeLists.txt @@ -71,6 +71,8 @@ corrade_add_test(BasisImporterTest BasisImporterTest.cpp invalid-cube-face-size.basis rgb.basis rgb.ktx2 + rgbf.basis + rgbf.ktx2 rgb-pow2.basis rgb-pow2.ktx2 rgb-linear.basis @@ -82,6 +84,8 @@ corrade_add_test(BasisImporterTest BasisImporterTest.cpp rgb-noflip-pow2.ktx2 rgba.basis rgba.ktx2 + rgbaf.basis + rgbaf.ktx2 rgba-pow2.basis rgba-pow2.ktx2 rgba-uastc.basis @@ -105,7 +109,9 @@ corrade_add_test(BasisImporterTest BasisImporterTest.cpp rgba-video-uastc.basis rgba-video-uastc.ktx2 rgb-63x27.png + rgb-63x27.exr rgba-63x27.png + rgba-63x27.exr rgba-63x27-slice1.png rgba-63x27-slice2.png rgba-31x13.png From 3eb2b84e9d9c41c434ecdd26c0fc35df1d639fe8 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Sun, 29 Sep 2024 14:55:30 +0200 Subject: [PATCH 07/10] BasisImporter: set alignment to 1 for unaligned formats Instead of an unavoidable copy. If users want 4-byte alignment, they can choose to perform the copy themselves. Most other importers do the same thing. --- .../BasisImporter/BasisImporter.cpp | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp index f8869b01a..3ab8147ff 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp @@ -651,9 +651,6 @@ template Containers::Optional> Bas rowStrideInBlocksOrPixels = 0; /* left up to Basis to calculate */ outputRowsInPixels = 0; /* not used for compressed data */ outputSizeInBlocksOrPixels = totalBlocks; - /* All compressed formats have a size that matches the default 4-byte - alignment of CompressedPixelStorage */ - CORRADE_INTERNAL_ASSERT(formatSize % 4 == 0); } const UnsignedInt sliceSize = formatSize*outputSizeInBlocksOrPixels; @@ -691,30 +688,12 @@ template Containers::Optional> Bas } if(isUncompressed) { - const Magnum::PixelFormat outFormat = pixelFormat(*targetFormat, _state->isSrgb); - const Math::Vector outSize = Math::Vector::pad(Vector3i{size}); - /* All (compressed or uncompressed) formats have a block/pixel size - that's a multiple of 4, matching the PixelStorage default alignment - of 4. *Except* for RGB16F. Basis transcoding functions only take row - stride in pixels, not bytes, so we need to manually copy to a - correctly aligned image. */ - const UnsignedInt rowWidthInBytes = formatSize*size.x(); - if(rowWidthInBytes % 4 != 0) { - const UnsignedInt rowStrideInBytes = (rowWidthInBytes + 3)/4*4; - const UnsignedInt alignedDataSize = rowStrideInBytes*size.y()*size.z(); - Containers::Array alignedDest{DefaultInit, alignedDataSize}; - - PixelStorage unalignedStorage; - unalignedStorage.setAlignment(1); - const BasicImageView src{unalignedStorage, outFormat, outSize, dest}; - const BasicMutableImageView dst{outFormat, outSize, alignedDest}; - - Utility::copy(src.pixels(), dst.pixels()); - - dest = Utility::move(alignedDest); - } + /* Adjust pixel storage if row size is not four byte aligned */ + PixelStorage storage; + if((formatSize*size.x())%4 != 0) + storage.setAlignment(1); - Trade::ImageData out{outFormat, outSize, Utility::move(dest), ImageFlag(UnsignedShort(_state->imageFlags))}; + Trade::ImageData out{storage, pixelFormat(*targetFormat, _state->isSrgb), Math::Vector::pad(Vector3i{size}), Utility::move(dest), ImageFlag(UnsignedShort(_state->imageFlags))}; /* Flip if needed */ if(!_state->isYFlipped) From 0690611d70f6175ad879733f649ea7449f4a91e4 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Sun, 29 Sep 2024 14:57:41 +0200 Subject: [PATCH 08/10] BasisImporter: make codecov happy --- src/MagnumPlugins/BasisImporter/BasisImporter.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp index 3ab8147ff..334aa6bdf 100644 --- a/src/MagnumPlugins/BasisImporter/BasisImporter.cpp +++ b/src/MagnumPlugins/BasisImporter/BasisImporter.cpp @@ -755,10 +755,12 @@ template Containers::Optional> Bas flipped = false; break; /* We'd be in the isUncompressed branch above for this */ + /* LCOV_EXCL_START */ case TargetFormat::RGBA8: case TargetFormat::RGB16F: case TargetFormat::RGBA16F: - CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + /* LCOV_EXCL_STOP */ } if(flipped && !(flags() & ImporterFlag::Quiet) && size.y() % blockSize.y() != 0) From 46f9115dca4998d17e3f28b7bb28c39d93535719 Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Sun, 29 Sep 2024 20:45:43 +0200 Subject: [PATCH 09/10] Basis{ImageConverter,Importer}: use conventional view slicing to avoid mutablePixels() --- .../Test/BasisImageConverterTest.cpp | 11 ++++++----- .../BasisImporter/Test/BasisImporterTest.cpp | 15 +++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp index f3d1871ad..40bdf34b4 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp +++ b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp @@ -989,9 +989,7 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { auto result = importer->image2D(0, i); CORRADE_VERIFY(result); - /* mutablePixels() so we can use the convenient slice syntax. Const - overloads of a()/rgb() return by value which makes it not work. */ - const auto pixels = result->mutablePixels(); + const auto pixels = result->pixels(); /* For HDR images alpha is always transcoded to 1. Can't use a StridedArrayView with broadcasted size since CompareImage only @@ -999,7 +997,10 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { using namespace Math::Literals; Containers::Array ones{DirectInit, size_t(result->size().product()), 1.0_h}; - CORRADE_COMPARE_WITH(pixels.slice(&Color4h::a), + /* Using this monstrosity to slice to the alpha channel since the + slice() overload taking a member function requires that getter to + return by reference, and the const overload of Color4::a() doesn't */ + CORRADE_COMPARE_WITH((Containers::arrayCast<2, const Half>(pixels).exceptPrefix({0, 3}).every({1, 4})), (ImageView2D{PixelStorage{}.setAlignment(1), PixelFormat::R16F, result->size(), ones}), /* No errors, always exactly 1.0 */ (DebugTools::CompareImage{0.0f, 0.0f})); @@ -1015,7 +1016,7 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { imageImporter->configuration().setValue("a", "A"); - CORRADE_COMPARE_WITH(pixels.slice(&Color4h::rgb), *expected, + CORRADE_COMPARE_WITH(Containers::arrayCast(pixels), *expected, /* There are moderately significant compression artifacts */ (DebugTools::CompareImage{levels[i].maxThreshold, levels[i].meanThreshold})); } diff --git a/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp b/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp index 2bbe9a93a..d09d2dce2 100644 --- a/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp +++ b/src/MagnumPlugins/BasisImporter/Test/BasisImporterTest.cpp @@ -764,9 +764,7 @@ void BasisImporterTest::unconfiguredHdr() { if(_manager.loadState("OpenExrImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("OpenExrImporter plugin not found, cannot test contents"); - /* mutablePixels() so we can use the convenient slice syntax. Const - overload of rgb() returns by value which makes it not work. */ - CORRADE_COMPARE_WITH(image->mutablePixels().slice(&Color4h::rgb), + CORRADE_COMPARE_WITH(Containers::arrayCast(image->pixels()), Utility::Path::join(BASISIMPORTER_TEST_DIR, "rgb-63x27.exr"), /* There are moderately significant compression artifacts */ (DebugTools::CompareImageToFile{_manager, 0.052f, 0.004f})); @@ -1096,9 +1094,7 @@ void BasisImporterTest::rgbaUncompressedHdr() { CORRADE_COMPARE(image->format(), PixelFormat::RGBA16F); CORRADE_COMPARE(image->size(), (Vector2i{63, 27})); - /* mutablePixels() so we can use the convenient slice syntax. Const - overloads of a()/rgb() return by value which makes it not work. */ - const auto pixels = image->mutablePixels(); + const auto pixels = image->pixels(); /* For HDR images alpha is always transcoded to 1. Can't use a StridedArrayView with broadcasted size since CompareImage only @@ -1106,7 +1102,10 @@ void BasisImporterTest::rgbaUncompressedHdr() { using namespace Math::Literals; Containers::Array ones{DirectInit, size_t(image->size().product()), 1.0_h}; - CORRADE_COMPARE_WITH(pixels.slice(&Color4h::a), + /* Using this monstrosity to slice to the alpha channel since the slice() + overload taking a member function requires that getter to return by + reference, and the const overload of Color4::a() doesn't */ + CORRADE_COMPARE_WITH((Containers::arrayCast<2, const Half>(pixels).exceptPrefix({0, 3}).every({1, 4})), (ImageView2D{PixelStorage{}.setAlignment(1), PixelFormat::R16F, image->size(), ones}), /* No errors, always exactly 1.0 */ (DebugTools::CompareImage{0.0f, 0.0f})); @@ -1129,7 +1128,7 @@ void BasisImporterTest::rgbaUncompressedHdr() { imageImporter->configuration().setValue("a", "A"); - CORRADE_COMPARE_WITH(pixels.slice(&Color4h::rgb), *expected, + CORRADE_COMPARE_WITH(Containers::arrayCast(pixels), *expected, /* There are moderately significant compression artifacts */ (DebugTools::CompareImage{0.637f, 0.009f})); } From 801e501beff427b911831777d0e41c0dff2eaa0b Mon Sep 17 00:00:00 2001 From: Pablo Escobar Date: Sun, 29 Sep 2024 20:58:24 +0200 Subject: [PATCH 10/10] BasisImageConverter: use two importer instances for better legibility --- .../Test/BasisImageConverterTest.cpp | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp index 40bdf34b4..4e6b0d43c 100644 --- a/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp +++ b/src/MagnumPlugins/BasisImageConverter/Test/BasisImageConverterTest.cpp @@ -935,11 +935,10 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { if(_manager.loadState("OpenExrImporter") == PluginManager::LoadState::NotFound) CORRADE_SKIP("OpenExrImporter plugin not found, cannot test contents"); - Containers::Pointer imageImporter = _manager.instantiate("OpenExrImporter"); - /* BasisImageConverter only reads 32-bit floats, but the test files are half float images. Let openexr do the conversion for us. */ - imageImporter->configuration().setValue("forceChannelType", "FLOAT"); + Containers::Pointer imageRGBA32FImporter = _manager.instantiate("OpenExrImporter"); + imageRGBA32FImporter->configuration().setValue("forceChannelType", "FLOAT"); struct Level { const char* file; @@ -954,19 +953,14 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { for(Level& level: levels) { CORRADE_ITERATION(level.file); - CORRADE_VERIFY(imageImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, level.file))); - Containers::Optional originalImage = imageImporter->image2D(0); + CORRADE_VERIFY(imageRGBA32FImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, level.file))); + Containers::Optional originalImage = imageRGBA32FImporter->image2D(0); CORRADE_VERIFY(originalImage); /* Use the original images and add a skip to ensure the converter reads the image data properly */ level.imageWithSkip = copyImageWithSkip(ImageView2D(*originalImage), {7, 8}); } - /* Revert back to loading the actual format. BasisImporter can't transcode - to 32-bit float, which is fine since the test files are half float as - well. */ - imageImporter->configuration().setValue("forceChannelType", ""); - Containers::Pointer converter = _converterManager.instantiate("BasisImageConverter"); /* HDR is encoded using UASTC HDR, but shouldn't require uastc to be @@ -984,6 +978,12 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { CORRADE_COMPARE(importer->image2DCount(), 1); CORRADE_COMPARE(importer->image2DLevelCount(0), Containers::arraySize(levels)); + /* For HDR images alpha is always transcoded to 1. Import source .exr files + again as half float RGB for easier comparison against transcoded RGB. */ + Containers::Pointer imageRGB16FImporter = _manager.instantiate("OpenExrImporter"); + /* Drop the alpha channel */ + imageRGB16FImporter->configuration().setValue("a", ""); + for(std::size_t i = 0; i != Containers::arraySize(levels); ++i) { CORRADE_ITERATION("level" << i); auto result = importer->image2D(0, i); @@ -991,9 +991,9 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { const auto pixels = result->pixels(); - /* For HDR images alpha is always transcoded to 1. Can't use a - StridedArrayView with broadcasted size since CompareImage only - accepts StridedArrayView for the actual image, not the expected. */ + /* Can't use a StridedArrayView with broadcasted size since + CompareImage only accepts StridedArrayView for the actual image, not + the expected. */ using namespace Math::Literals; Containers::Array ones{DirectInit, size_t(result->size().product()), 1.0_h}; @@ -1005,17 +1005,13 @@ void BasisImageConverterTest::convert2DMipmapsHdr() { /* No errors, always exactly 1.0 */ (DebugTools::CompareImage{0.0f, 0.0f})); - /* Drop alpha channel to only test RGB. Not using CompareImageToFile to - avoid overwriting the 4-channel expected .exr with a 3-channel - actual image when --save-diagnostic is specified. */ - imageImporter->configuration().setValue("a", ""); - - CORRADE_VERIFY(imageImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, levels[i].file))); - const auto expected = imageImporter->image2D(0); + /* Not using CompareImageToFile to avoid overwriting the 4-channel + expected .exr with a 3-channel actual image when --save-diagnostic + is specified */ + CORRADE_VERIFY(imageRGB16FImporter->openFile(Utility::Path::join(BASISIMPORTER_TEST_DIR, levels[i].file))); + const auto expected = imageRGB16FImporter->image2D(0); CORRADE_VERIFY(expected); - imageImporter->configuration().setValue("a", "A"); - CORRADE_COMPARE_WITH(Containers::arrayCast(pixels), *expected, /* There are moderately significant compression artifacts */ (DebugTools::CompareImage{levels[i].maxThreshold, levels[i].meanThreshold}));