From 79c351904512b37733f0843925d3e8987972a981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 24 Jan 2021 12:09:13 +0100 Subject: [PATCH] Vk: add driver workaround scaffolding and the first one for SwiftShader. Yay? It's funny, what produces correct result causes validation errors. I suppose this will be very similar for all other workarounds. --- doc/opengl-workarounds.dox | 6 +- doc/vulkan-workarounds.dox | 58 +++++++++ doc/vulkan.dox | 6 + .../GL/Implementation/driverSpecific.cpp | 4 +- src/Magnum/Vk/CMakeLists.txt | 2 + src/Magnum/Vk/CommandBuffer.h | 3 + src/Magnum/Vk/Device.cpp | 54 ++++++++- src/Magnum/Vk/Device.h | 14 ++- src/Magnum/Vk/Image.cpp | 65 ++++++++++ src/Magnum/Vk/Implementation/Arguments.cpp | 4 +- src/Magnum/Vk/Implementation/DeviceState.cpp | 23 +++- src/Magnum/Vk/Implementation/DeviceState.h | 4 +- .../Vk/Implementation/DriverWorkaround.cpp | 114 ++++++++++++++++++ .../Vk/Implementation/DriverWorkaround.h | 43 +++++++ src/Magnum/Vk/Instance.cpp | 5 + src/Magnum/Vk/Instance.h | 13 ++ src/Magnum/Vk/Test/DeviceVkTest.cpp | 73 ++++++++++- src/Magnum/Vk/Test/ImageVkTest.cpp | 91 ++++++++++++++ 18 files changed, 563 insertions(+), 19 deletions(-) create mode 100644 doc/vulkan-workarounds.dox create mode 100644 src/Magnum/Vk/Implementation/DriverWorkaround.cpp create mode 100644 src/Magnum/Vk/Implementation/DriverWorkaround.h diff --git a/doc/opengl-workarounds.dox b/doc/opengl-workarounds.dox index 5d9196e3f..7cdc9fec1 100644 --- a/doc/opengl-workarounds.dox +++ b/doc/opengl-workarounds.dox @@ -46,8 +46,8 @@ Using driver workarounds: ... @endcode -These identifiers correspond to the strings in the below listing. For debugging -and diagnostic purpose it's possible to disable particular workarounds by +These identifiers correspond to the strings in the listing below. For debugging +and diagnostic purposes it's possible to disable particular workarounds by passing their identifier string to the `--magnum-disable-workarounds` command-line option. See @ref GL-Context-command-line for more information. @@ -55,7 +55,7 @@ command-line option. See @ref GL-Context-command-line for more information. @snippet src/Magnum/GL/Implementation/driverSpecific.cpp workarounds -Chromium has a similar list: https://cs.chromium.org/chromium/src/gpu/config/gpu_driver_bug_list.json +Chromium has a similar list: https://cs.chromium.org/chromium/src/gpu/config/gpu_driver_bug_list.json . See also @ref vulkan-workarounds "Vulkan driver workarounds". */ } diff --git a/doc/vulkan-workarounds.dox b/doc/vulkan-workarounds.dox new file mode 100644 index 000000000..fb248f8f8 --- /dev/null +++ b/doc/vulkan-workarounds.dox @@ -0,0 +1,58 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +namespace Magnum { +/** @page vulkan-workarounds Driver workarounds +@brief List of Vulkan driver workarounds used by Magnum +@m_since_latest + +@m_footernavigation + +Driver workarounds used by a particular app are listed in the engine startup +log such as here: + +@code{.shell-session} +Device: SwiftShader Device (LLVM 10.0.0) +Device version: Vulkan 1.1 +Enabled device extensions: + VK_KHR_create_renderpass2 + ... +Using driver workarounds: + swiftshader-image-copy-extent-instead-of-layers + ... +@endcode + +These identifiers correspond to the strings in the listing below. For debugging +and diagnostic purposes it's possible to disable particular workarounds by +passing their identifier string to the `--magnum-disable-workarounds` +command-line option. See @ref Vk-Instance-command-line for more information. + +@m_class{m-console-wrap} + +@snippet src/Magnum/Vk/Implementation/DriverWorkaround.cpp workarounds + +@see @ref opengl-workarounds +*/ +} diff --git a/doc/vulkan.dox b/doc/vulkan.dox index 0f357b670..dd0fdfc96 100644 --- a/doc/vulkan.dox +++ b/doc/vulkan.dox @@ -43,6 +43,12 @@ following table. - @subpage vulkan-support +While not as bad as with OpenGL, neither the Vulkan driver world is ideal. In +order to work on majority of platforms, the engine has to work around some +driver bugs. An exhaustive list is here: + +- @subpage vulkan-workarounds + @section vulkan-required-extensions Version and extension requirements The engine can work on unextended Vulkan 1.0, but some specific functionality diff --git a/src/Magnum/GL/Implementation/driverSpecific.cpp b/src/Magnum/GL/Implementation/driverSpecific.cpp index a85569ce0..370c97fd3 100644 --- a/src/Magnum/GL/Implementation/driverSpecific.cpp +++ b/src/Magnum/GL/Implementation/driverSpecific.cpp @@ -482,8 +482,10 @@ auto Context::detectedDriver() -> DetectedDrivers { void Context::disableDriverWorkaround(const std::string& workaround) { /* Ignore unknown workarounds */ + /** @todo this will probably cause false positives when both GL and Vulkan + is used together? */ if(std::find(std::begin(KnownWorkarounds), std::end(KnownWorkarounds), workaround) == std::end(KnownWorkarounds)) { - Warning() << "Unknown workaround" << workaround; + Warning() << "GL: unknown workaround" << workaround; return; } _driverWorkarounds.emplace_back(workaround, true); diff --git a/src/Magnum/Vk/CMakeLists.txt b/src/Magnum/Vk/CMakeLists.txt index 4bb367beb..0bf36a08e 100644 --- a/src/Magnum/Vk/CMakeLists.txt +++ b/src/Magnum/Vk/CMakeLists.txt @@ -41,6 +41,7 @@ set(MagnumVk_SRCS Implementation/Arguments.cpp Implementation/DeviceState.cpp + Implementation/DriverWorkaround.cpp Implementation/InstanceState.cpp) set(MagnumVk_GracefulAssert_SRCS @@ -106,6 +107,7 @@ set(MagnumVk_PRIVATE_HEADERS Implementation/Arguments.h Implementation/DeviceFeatures.h Implementation/DeviceState.h + Implementation/DriverWorkaround.h Implementation/InstanceState.h Implementation/compressedPixelFormatMapping.hpp diff --git a/src/Magnum/Vk/CommandBuffer.h b/src/Magnum/Vk/CommandBuffer.h index 2cd4005f6..32d26e3a9 100644 --- a/src/Magnum/Vk/CommandBuffer.h +++ b/src/Magnum/Vk/CommandBuffer.h @@ -726,12 +726,15 @@ class MAGNUM_VK_EXPORT CommandBuffer { MAGNUM_VK_LOCAL static void copyBufferImplementationKHR(CommandBuffer& self, const CopyBufferInfo& info); MAGNUM_VK_LOCAL static void copyImageImplementationDefault(CommandBuffer& self, const CopyImageInfo& info); + MAGNUM_VK_LOCAL static void copyImageImplementationSwiftShader(CommandBuffer& self, const CopyImageInfo& info); MAGNUM_VK_LOCAL static void copyImageImplementationKHR(CommandBuffer& self, const CopyImageInfo& info); MAGNUM_VK_LOCAL static void copyBufferToImageImplementationDefault(CommandBuffer& self, const CopyBufferToImageInfo& info); + MAGNUM_VK_LOCAL static void copyBufferToImageImplementationSwiftShader(CommandBuffer& self, const CopyBufferToImageInfo& info); MAGNUM_VK_LOCAL static void copyBufferToImageImplementationKHR(CommandBuffer& self, const CopyBufferToImageInfo& info); MAGNUM_VK_LOCAL static void copyImageToBufferImplementationDefault(CommandBuffer& self, const CopyImageToBufferInfo& info); + MAGNUM_VK_LOCAL static void copyImageToBufferImplementationSwiftShader(CommandBuffer& self, const CopyImageToBufferInfo& info); MAGNUM_VK_LOCAL static void copyImageToBufferImplementationKHR(CommandBuffer& self, const CopyImageToBufferInfo& info); /* Can't be a reference because of the NoCreate constructor */ diff --git a/src/Magnum/Vk/Device.cpp b/src/Magnum/Vk/Device.cpp index 01c5a5868..5764a5687 100644 --- a/src/Magnum/Vk/Device.cpp +++ b/src/Magnum/Vk/Device.cpp @@ -50,6 +50,7 @@ #include "Magnum/Vk/Implementation/InstanceState.h" #include "Magnum/Vk/Implementation/DeviceFeatures.h" #include "Magnum/Vk/Implementation/DeviceState.h" +#include "Magnum/Vk/Implementation/DriverWorkaround.h" #include "Magnum/Vk/Implementation/structureHelpers.h" #include "MagnumExternal/Vulkan/flextVkGlobal.h" @@ -103,6 +104,9 @@ struct DeviceCreateInfo::State { Containers::String disabledExtensionsStorage; Containers::Array disabledExtensions; + /* .second = true means the workaround is disabled; the views always point + to the internal KnownWorkarounds array */ + Containers::Array> encounteredWorkarounds; Containers::Array queues; Containers::StaticArray<32, Float> queuePriorities; Containers::StaticArray<32, Queue*> queueOutput; @@ -131,6 +135,18 @@ DeviceCreateInfo::DeviceCreateInfo(DeviceProperties& deviceProperties, const Ext version via --magnum-vulkan-version, which will be later used to cap available features. */ _state->version = Version(Math::min(UnsignedInt(deviceProperties._instance->version()), UnsignedInt(deviceProperties.version()))); + /* If there are any disabled workarounds, save them until initialize() uses + them on device creation. The disableWorkaround() function saves the + internal string view instead of the one passed from the command line so + we don't need to bother with String allocations. */ + Containers::StringView disabledWorkarounds = args.value("disable-workarounds"); + if(!disabledWorkarounds.isEmpty()) { + Containers::Array split = disabledWorkarounds.splitWithoutEmptyParts(); + arrayReserve(_state->encounteredWorkarounds, split.size()); + for(Containers::StringView workaround: split) + Implementation::disableWorkaround(_state->encounteredWorkarounds, workaround); + } + /* If there are any disabled extensions, sort them and save for later -- we'll use them to filter the ones added by the app */ Containers::String disabledExtensions = args.value("disable-extensions"); @@ -622,7 +638,11 @@ void Device::wrap(Instance& instance, const VkPhysicalDevice physicalDevice, con _flags = flags; _properties.emplace(DeviceProperties::wrap(instance, physicalDevice)); initializeExtensions(enabledExtensions); - initialize(instance, version, enabledFeatures); + + /* Because we have no control over extensions / features, no workarounds + are used here -- better to just do nothing than just a partial attempt */ + Containers::Array> encounteredWorkarounds = Implementation::disableAllWorkarounds(); + initialize(instance, version, encounteredWorkarounds, enabledFeatures); } void Device::wrap(Instance& instance, const VkPhysicalDevice physicalDevice, const VkDevice handle, const Version version, const std::initializer_list enabledExtensions, const DeviceFeatures& enabledFeatures, const HandleFlags flags) { @@ -720,8 +740,34 @@ Result Device::tryCreateInternal(Instance& instance, const DeviceCreateInfo& inf return Result(result); } + /* Make a copy of the workarounds list coming from DeviceCreateInfo as + initialize() may modify it */ + /** @todo switch to Containers::Pair once it exists and use Utility::copy() + (std::pair isn't trivially copyable, ffs) */ + Containers::Array> encounteredWorkarounds{info._state->encounteredWorkarounds.size()}; + for(std::size_t i = 0; i != encounteredWorkarounds.size(); ++i) + encounteredWorkarounds[i] = info._state->encounteredWorkarounds[i]; + + /* Initialize the enabled extension list and feature-, extension-, + workaround-dependent function pointers */ initializeExtensions({info->ppEnabledExtensionNames, info->enabledExtensionCount}); - initialize(instance, version, info._state->enabledFeatures | info._state->implicitFeatures); + initialize(instance, version, encounteredWorkarounds, info._state->enabledFeatures | info._state->implicitFeatures); + + /* Print a list of used workarounds */ + if(!info._state->quietLog) { + bool workaroundHeaderPrinted = false; + for(const auto& workaround: encounteredWorkarounds) { + /* Skip disabled workarounds */ + if(workaround.second) continue; + + if(!workaroundHeaderPrinted) { + workaroundHeaderPrinted = true; + Debug{} << "Using device driver workarounds:"; + } + + Debug{} << " " << workaround.first; + } + } #ifndef CORRADE_NO_ASSERT /* This is a dumb O(n^2) search but in an assert that's completely fine */ @@ -789,14 +835,14 @@ template void Device::initializeExtensions(const Containers::ArrayView< } } -void Device::initialize(Instance& instance, const Version version, const DeviceFeatures& enabledFeatures) { +void Device::initialize(Instance& instance, const Version version, Containers::Array>& encounteredWorkarounds, const DeviceFeatures& enabledFeatures) { /* Init version, features, function pointers */ _version = version; _enabledFeatures = enabledFeatures; flextVkInitDevice(_handle, &_functionPointers, instance->GetDeviceProcAddr); /* Set up extension-dependent functionality */ - _state.emplace(*this); + _state.emplace(*this, encounteredWorkarounds); } bool Device::isExtensionEnabled(const Extension& extension) const { diff --git a/src/Magnum/Vk/Device.h b/src/Magnum/Vk/Device.h index 000b7ab39..581871a16 100644 --- a/src/Magnum/Vk/Device.h +++ b/src/Magnum/Vk/Device.h @@ -151,6 +151,9 @@ The @ref Device inherits a subset of the the following. If the @ref Instance didn't get `argc` / `argv` passed, only the environment variables are used. +- `--magnum-disable-workarounds LIST` --- Vulkan driver workarounds to + disable (see @ref vulkan-workarounds for detailed info) (environment: + `MAGNUM_DISABLE_WORKAROUNDS`) - `--magnum-disable-extensions LIST` --- Vulkan instance or device extensions to disable, meaning @ref DeviceCreateInfo::addEnabledExtensions() will skip them (environment: `MAGNUM_DISABLE_EXTENSIONS`) @@ -241,6 +244,15 @@ class MAGNUM_VK_EXPORT Device { * @p enabledExtensions / @p enabledFeatures is empty, the device will * behave as if no extensions / no features were enabled. * + * @m_class{m-note m-danger} + * + * @par + * Due to the extension / feature list being outside of library + * control here, driver bug workarounds are not detected and + * enabled when using this function. Depending on bug severity, + * that may lead to crashes and unexpected behavior that wouldn't + * otherwise happen with a @ref Device created the usual way. + * * Note that this function retrieves all device-specific Vulkan * function pointers, which is a relatively costly operation. It's thus * not recommended to call this function repeatedly for creating @@ -480,7 +492,7 @@ class MAGNUM_VK_EXPORT Device { Result tryCreateInternal(Instance& isntance, const DeviceCreateInfo&, DeviceProperties&&); template MAGNUM_VK_LOCAL void initializeExtensions(Containers::ArrayView enabledExtensions); - MAGNUM_VK_LOCAL void initialize(Instance& instance, Version version, const DeviceFeatures& enabledFeatures); + MAGNUM_VK_LOCAL void initialize(Instance& instance, Version version, Containers::Array>& encounteredWorkarounds, const DeviceFeatures& enabledFeatures); MAGNUM_VK_LOCAL static void getQueueImplementationDefault(Device& self, const VkDeviceQueueInfo2& info, VkQueue& queue); MAGNUM_VK_LOCAL static void getQueueImplementation11(Device& self, const VkDeviceQueueInfo2& info, VkQueue& queue); diff --git a/src/Magnum/Vk/Image.cpp b/src/Magnum/Vk/Image.cpp index 40a810409..d05dc312d 100644 --- a/src/Magnum/Vk/Image.cpp +++ b/src/Magnum/Vk/Image.cpp @@ -634,12 +634,53 @@ CommandBuffer& CommandBuffer::copyImage(const CopyImageInfo& info) { return *this; } +namespace { + +/* See the "swiftshader-image-copy-extent-instead-of-layers" workaround for + more info */ +void fixupImageCopySwiftShader(VkImageSubresourceLayers& subresource, VkOffset3D& offset, VkExtent3D& extent) { + /* Not a layered image, nothing to do */ + if(subresource.baseArrayLayer == 0 && subresource.layerCount == 1) return; + + /* When copying 2D array to 3D, depth is already at the value we want it to + be */ + CORRADE_INTERNAL_ASSERT(offset.z == 0 && (extent.depth == 1 || extent.depth == subresource.layerCount)); + + /* Put the layer info into the third extent dimension instead of the layer + fields, as those seem to be interpreted in a wrong way. However those + still need to be set to values that make sense in total, otherwise nasty + crashes happen. + + Fortunately this works for 1D array images as well, and we don't need to + do extra voodoo to detect if the image is 1D to use y / width instead of + z / depth. */ + offset.z = subresource.baseArrayLayer; + extent.depth = subresource.layerCount; + subresource.baseArrayLayer = 0; + subresource.layerCount = 1; +} + +} + void CommandBuffer::copyImageImplementationDefault(CommandBuffer& self, const CopyImageInfo& info) { CORRADE_ASSERT(!info->pNext, "Vk::CommandBuffer::copyImage(): disallowing extraction of CopyImageInfo with non-empty pNext to prevent information loss", ); return (**self._device).CmdCopyImage(self, info->srcImage, info->srcImageLayout, info->dstImage, info->dstImageLayout, info->regionCount, info.vkImageCopies()); } +void CommandBuffer::copyImageImplementationSwiftShader(CommandBuffer& self, const CopyImageInfo& info) { + CORRADE_ASSERT(!info->pNext, + "Vk::CommandBuffer::copyImage(): disallowing extraction of CopyImageInfo with non-empty pNext to prevent information loss", ); + + Containers::Array copies = info.vkImageCopies(); + for(VkImageCopy& copy: copies) { + fixupImageCopySwiftShader(copy.srcSubresource, copy.srcOffset, copy.extent); + fixupImageCopySwiftShader(copy.dstSubresource, copy.dstOffset, copy.extent); + } + + return (**self._device).CmdCopyImage(self, info->srcImage, info->srcImageLayout, info->dstImage, info->dstImageLayout, info->regionCount, copies); +} + void CommandBuffer::copyImageImplementationKHR(CommandBuffer& self, const CopyImageInfo& info) { return (**self._device).CmdCopyImage2KHR(self, info); } @@ -655,6 +696,18 @@ void CommandBuffer::copyBufferToImageImplementationDefault(CommandBuffer& self, return (**self._device).CmdCopyBufferToImage(self, info->srcBuffer, info->dstImage, info->dstImageLayout, info->regionCount, info.vkBufferImageCopies()); } +void CommandBuffer::copyBufferToImageImplementationSwiftShader(CommandBuffer& self, const CopyBufferToImageInfo& info) { + CORRADE_ASSERT(!info->pNext, + "Vk::CommandBuffer::copyBufferToImage(): disallowing extraction of CopyBufferToImageInfo with non-empty pNext to prevent information loss", ); + + Containers::Array copies = info.vkBufferImageCopies(); + for(VkBufferImageCopy& copy: copies) { + fixupImageCopySwiftShader(copy.imageSubresource, copy.imageOffset, copy.imageExtent); + } + + return (**self._device).CmdCopyBufferToImage(self, info->srcBuffer, info->dstImage, info->dstImageLayout, info->regionCount, copies); +} + void CommandBuffer::copyBufferToImageImplementationKHR(CommandBuffer& self, const CopyBufferToImageInfo& info) { return (**self._device).CmdCopyBufferToImage2KHR(self, info); } @@ -670,6 +723,18 @@ void CommandBuffer::copyImageToBufferImplementationDefault(CommandBuffer& self, return (**self._device).CmdCopyImageToBuffer(self, info->srcImage, info->srcImageLayout, info->dstBuffer, info->regionCount, info.vkBufferImageCopies()); } +void CommandBuffer::copyImageToBufferImplementationSwiftShader(CommandBuffer& self, const CopyImageToBufferInfo& info) { + CORRADE_ASSERT(!info->pNext, + "Vk::CommandBuffer::copyImageToBuffer(): disallowing extraction of CopyImageToBufferInfo with non-empty pNext to prevent information loss", ); + + Containers::Array copies = info.vkBufferImageCopies(); + for(VkBufferImageCopy& copy: copies) { + fixupImageCopySwiftShader(copy.imageSubresource, copy.imageOffset, copy.imageExtent); + } + + return (**self._device).CmdCopyImageToBuffer(self, info->srcImage, info->srcImageLayout, info->dstBuffer, info->regionCount, copies); +} + void CommandBuffer::copyImageToBufferImplementationKHR(CommandBuffer& self, const CopyImageToBufferInfo& info) { return (**self._device).CmdCopyImageToBuffer2KHR(self, info); } diff --git a/src/Magnum/Vk/Implementation/Arguments.cpp b/src/Magnum/Vk/Implementation/Arguments.cpp index 8757eca6b..c44239527 100644 --- a/src/Magnum/Vk/Implementation/Arguments.cpp +++ b/src/Magnum/Vk/Implementation/Arguments.cpp @@ -31,7 +31,8 @@ namespace Magnum { namespace Vk { namespace Implementation { Utility::Arguments arguments() { Utility::Arguments args{"magnum"}; - args.addOption("disable-layers").setHelp("disable-layers", "Vulkan layers to disable", "LIST") + args.addOption("disable-workarounds").setHelp("disable-workarounds", "Vulkan driver workarounds to disable\n (see https://doc.magnum.graphics/magnum/vulkan-workarounds.html for detailed info)", "LIST") + .addOption("disable-layers").setHelp("disable-layers", "Vulkan layers to disable", "LIST") .addOption("disable-extensions").setHelp("disable-extensions", "Vulkan instance or device extensions to disable", "LIST") .addOption("enable-layers").setHelp("enable-layers", "Vulkan layers to enable in addition to the defaults and what the application requests", "LIST") .addOption("enable-instance-extensions").setHelp("enable-instance-extensions", "Vulkan instance extensions to enable in addition to the defaults and what the application requests", "LIST") @@ -39,6 +40,7 @@ Utility::Arguments arguments() { .addOption("vulkan-version").setHelp("vulkan-version", "force Vulkan version", "X.Y") .addOption("log", "default").setHelp("log", "console logging", "default|quiet|verbose") .addOption("device").setHelp("device", "device to use", "ID|integrated|discrete|virtual|cpu") + .setFromEnvironment("disable-workarounds") .setFromEnvironment("disable-layers") .setFromEnvironment("disable-extensions") .setFromEnvironment("enable-layers") diff --git a/src/Magnum/Vk/Implementation/DeviceState.cpp b/src/Magnum/Vk/Implementation/DeviceState.cpp index 459b80382..ffbdd3e4b 100644 --- a/src/Magnum/Vk/Implementation/DeviceState.cpp +++ b/src/Magnum/Vk/Implementation/DeviceState.cpp @@ -28,14 +28,18 @@ #include "Magnum/Vk/Buffer.h" #include "Magnum/Vk/CommandBuffer.h" #include "Magnum/Vk/Device.h" +#include "Magnum/Vk/DeviceProperties.h" #include "Magnum/Vk/Extensions.h" #include "Magnum/Vk/Image.h" #include "Magnum/Vk/RenderPass.h" #include "Magnum/Vk/Version.h" +#include "Magnum/Vk/Implementation/DriverWorkaround.h" namespace Magnum { namespace Vk { namespace Implementation { -DeviceState::DeviceState(Device& device) { +using namespace Containers::Literals; + +DeviceState::DeviceState(Device& device, Containers::Array>& encounteredWorkarounds) { if(device.isVersionSupported(Version::Vk11)) { getDeviceQueueImplementation = &Device::getQueueImplementation11; } else { @@ -88,9 +92,20 @@ DeviceState::DeviceState(Device& device) { cmdCopyImageToBufferImplementation = &CommandBuffer::copyImageToBufferImplementationKHR; } else { cmdCopyBufferImplementation = &CommandBuffer::copyBufferImplementationDefault; - cmdCopyImageImplementation = &CommandBuffer::copyImageImplementationDefault; - cmdCopyBufferToImageImplementation = &CommandBuffer::copyBufferToImageImplementationDefault; - cmdCopyImageToBufferImplementation = &CommandBuffer::copyImageToBufferImplementationDefault; + + /* SwiftShader doesn't implement KHR_copy_commands2 yet so we only need + to work around the classical code path. When it will, the image + array tests will blow up, notifying about this omission (though I + hope the bug gets fixed before KHR_copy_commands2 are implemented). */ + if(device.properties().name().hasPrefix("SwiftShader"_s) && !Implementation::isDriverWorkaroundDisabled(encounteredWorkarounds, "swiftshader-image-copy-extent-instead-of-layers"_s)) { + cmdCopyImageImplementation = &CommandBuffer::copyImageImplementationSwiftShader; + cmdCopyBufferToImageImplementation = &CommandBuffer::copyBufferToImageImplementationSwiftShader; + cmdCopyImageToBufferImplementation = &CommandBuffer::copyImageToBufferImplementationSwiftShader; + } else { + cmdCopyImageImplementation = &CommandBuffer::copyImageImplementationDefault; + cmdCopyBufferToImageImplementation = &CommandBuffer::copyBufferToImageImplementationDefault; + cmdCopyImageToBufferImplementation = &CommandBuffer::copyImageToBufferImplementationDefault; + } } } diff --git a/src/Magnum/Vk/Implementation/DeviceState.h b/src/Magnum/Vk/Implementation/DeviceState.h index f25f99481..7bd797646 100644 --- a/src/Magnum/Vk/Implementation/DeviceState.h +++ b/src/Magnum/Vk/Implementation/DeviceState.h @@ -25,6 +25,8 @@ DEALINGS IN THE SOFTWARE. */ +#include + #include "Magnum/Vk/Vk.h" #include "Magnum/Vk/Vulkan.h" @@ -37,7 +39,7 @@ class RenderPassCreateInfo; namespace Implementation { struct DeviceState { - explicit DeviceState(Device& device); + explicit DeviceState(Device& device, Containers::Array>& encounteredWorkarounds); void(*getDeviceQueueImplementation)(Device&, const VkDeviceQueueInfo2&, VkQueue&); diff --git a/src/Magnum/Vk/Implementation/DriverWorkaround.cpp b/src/Magnum/Vk/Implementation/DriverWorkaround.cpp new file mode 100644 index 000000000..b8f9303fb --- /dev/null +++ b/src/Magnum/Vk/Implementation/DriverWorkaround.cpp @@ -0,0 +1,114 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/Vk/Implementation/DriverWorkaround.h" + +namespace Magnum { namespace Vk { namespace Implementation { + +namespace { + +using namespace Containers::Literals; + +/* Search the code for the following strings to see where they are implemented */ +constexpr Containers::StringView KnownWorkarounds[]{ +/* [workarounds] */ +/* For layered image copies, SwiftShader (5.0? the version reporting is messy) + expects the layer offsets/counts to be included as second/third dimension of + the image offset/extent instead. Actually, having the Vulkan API contain + just 3D offset and extent with no layer offset/count would make more sense + to me as well -- the last dimension can be either in the offset/extent or + layer offset/count, but never in both, so the extra fields feel redundant. + Or maybe it's reserving space for layered 3D images? */ +"swiftshader-image-copy-extent-instead-of-layers"_s, +/* [workarounds] */ +}; + +/* I could use std::find(), right? Well, it'd be a whole lot more typing and + an #include *and* #include or whatever as well, + because apparently ONE CAN'T GET std::begin() / std::end() without including + tens thousands lines of irrelevant shit, FFS. + + Also the comparison to array end to discover if it wasn't found is just a + useless verbose crap shit as well, so we'll do better here and return a null + view instead. + + Moreover, based on the experience with GL, I don't expect there being too + many workarounds used heavily (10 at most, maybe?) so I won't bother with + some binary search, which needs extra testing effort. */ +Containers::StringView findWorkaround(Containers::StringView workaround) { + for(Containers::StringView i: KnownWorkarounds) + if(workaround == i) return i; + return {}; +} + +} + +void disableWorkaround(Containers::Array>& encounteredWorkarounds, const Containers::StringView workaround) { + /* Find the workaround. Note that we'll add the found view to the array + and not the passed view, as the found view is guaranteed to stay in + scope */ + Containers::StringView found = findWorkaround(workaround); + + /* Ignore unknown workarounds */ + /** @todo this will probably cause false positives when both GL and Vulkan + is used together? */ + if(found.isEmpty()) { + Warning{} << "Vk: unknown workaround" << workaround; + return; + } + + arrayAppend(encounteredWorkarounds, Containers::InPlaceInit, found, true); +} + +Containers::Array> disableAllWorkarounds() { + Containers::Array> encounteredWorkarounds; + for(Containers::StringView i: KnownWorkarounds) + arrayAppend(encounteredWorkarounds, Containers::InPlaceInit, i, true); + return encounteredWorkarounds; +} + +bool isDriverWorkaroundDisabled(Containers::Array>& encounteredWorkarounds, const Containers::StringView workaround) { + /* Find the workaround. Note that we'll add the found view to the array + and not the passed view, as the found view is guaranteed to stay in + scope */ + Containers::StringView found = findWorkaround(workaround); + CORRADE_INTERNAL_ASSERT(!found.isEmpty()); + + /* If the workaround was already asked for or disabled, return its state, + otherwise add it to the list as used one. Here we again cheat a bit and + compare just data pointers instead of the whole string as we store only + the views in the KnownWorkarounds list. */ + for(const auto& i: encounteredWorkarounds) + if(i.first.data() == found.data()) return i.second; + arrayAppend(encounteredWorkarounds, Containers::InPlaceInit, found, false); + return false; +} + +}}} diff --git a/src/Magnum/Vk/Implementation/DriverWorkaround.h b/src/Magnum/Vk/Implementation/DriverWorkaround.h new file mode 100644 index 000000000..971a90069 --- /dev/null +++ b/src/Magnum/Vk/Implementation/DriverWorkaround.h @@ -0,0 +1,43 @@ +#ifndef Magnum_Vk_Implementation_DriverWorkaround_h +#define Magnum_Vk_Implementation_DriverWorkaround_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "Magnum/Magnum.h" + +namespace Magnum { namespace Vk { namespace Implementation { + +void disableWorkaround(Containers::Array>& encounteredWorkarounds, Containers::StringView workaround); + +bool isDriverWorkaroundDisabled(Containers::Array>& encounteredWorkarounds, Containers::StringView workaround); + +/* Used by Device::wrap() -- because device extension setup is outside of our + control and the function doesn't print anything on the output, it's better + to just do nothing at all than silently enabling some subset */ +Containers::Array> disableAllWorkarounds(); + +}}} + +#endif diff --git a/src/Magnum/Vk/Instance.cpp b/src/Magnum/Vk/Instance.cpp index 9a2bf0dd9..dcc30c0ea 100644 --- a/src/Magnum/Vk/Instance.cpp +++ b/src/Magnum/Vk/Instance.cpp @@ -91,6 +91,11 @@ InstanceCreateInfo::InstanceCreateInfo(const Int argc, const char** const argv, _state->version = enumerateInstanceVersion(); _applicationInfo.apiVersion = UnsignedInt(_state ? _state->version : enumerateInstanceVersion()); + /** @todo handle disabled workarounds once we need them on the instance + level as well -- also ensure that warnings about unknown workarounds are + printed just once (silence the output on one side), or not? since we're + printing two lists anyway */ + /* If there are any disabled layers or extensions, sort them and save for later -- we'll use them to filter the ones added by the app */ Containers::String disabledLayers = args.value("disable-layers"); diff --git a/src/Magnum/Vk/Instance.h b/src/Magnum/Vk/Instance.h index 2622a7081..2e2918970 100644 --- a/src/Magnum/Vk/Instance.h +++ b/src/Magnum/Vk/Instance.h @@ -110,6 +110,7 @@ is reused by a subsequently created @ref Device as well. @code{.sh} [--magnum-help] + [--magnum-disable-workarounds LIST] [--magnum-disable-layers LIST] [--magnum-disable-extensions LIST] [--magnum-enable-layers LIST] @@ -124,6 +125,9 @@ Arguments: - `...` --- main application arguments (see `-h` or `--help` for details) - `--magnum-help` --- display this help message and exit +- `--magnum-disable-workarounds LIST` --- Vulkan driver workarounds to + disable (see @ref vulkan-workarounds for detailed info) (environment: + `MAGNUM_DISABLE_WORKAROUNDS`) - `--magnum-disable-layers LIST` --- Vulkan layers to disable, meaning @ref InstanceCreateInfo::addEnabledLayers() will skip them (environment: `MAGNUM_DISABLE_LAYERS`) @@ -216,6 +220,15 @@ class MAGNUM_VK_EXPORT Instance { * things. If @p enabledExtensions empty, the instance will behave as * if no extensions were enabled. * + * @m_class{m-note m-danger} + * + * @par + * Due to the extension and layer list being outside of library + * control here, driver bug workarounds are not detected and + * enabled when using this function. Depending on bug severity, + * that may lead to crashes and unexpected behavior that wouldn't + * otherwise happen with an @ref Instance created the usual way. + * * Note that this function retrieves all instance-specific Vulkan * function pointers, which is a relatively costly operation. It's thus * not recommended to call this function repeatedly for creating diff --git a/src/Magnum/Vk/Test/DeviceVkTest.cpp b/src/Magnum/Vk/Test/DeviceVkTest.cpp index 77bce304b..ca179edf9 100644 --- a/src/Magnum/Vk/Test/DeviceVkTest.cpp +++ b/src/Magnum/Vk/Test/DeviceVkTest.cpp @@ -78,6 +78,7 @@ struct DeviceVkTest: VulkanTester { void constructTransferDeviceProperties(); void constructExtensionsCommandLineDisable(); void constructExtensionsCommandLineEnable(); + void constructWorkaroundsCommandLineDisable(); void constructMultipleQueues(); void constructRawQueue(); void constructFeatureNotSupported(); @@ -104,8 +105,8 @@ struct { bool driverVersionSupported, debugMarkerEnabled, maintenance1Enabled; const char* log; } ConstructCommandLineData[] { - /* Shouldn't print anything about version, enabled layers/exts if quier - output is enabled. */ + /* Shouldn't print anything about device/version, enabled layers/exts if + quiet output is enabled. */ {"quiet", "quiet, enabled extensions", Containers::array({"", "--magnum-log", "quiet"}), Containers::array({"", "--magnum-log", "quiet", @@ -152,6 +153,29 @@ struct { "Device version: Vulkan {}.{}{}\n"}, }; +struct { + const char* name; + bool shouldPassAlways; + Containers::Array args; + const char* log; +} ConstructWorkaroundsCommandLineData[] { + {"default", false, nullptr, + "Device: {}\n" + "Device version: Vulkan {}.{}{}\n" + "Using device driver workarounds:\n" + " swiftshader-image-copy-extent-instead-of-layers\n"}, + /* Shouldn't print anything if quiet output is enabled */ + {"quiet", true, + Containers::array({"", + "--magnum-log", "quiet"}), + ""}, + {"disabled workarounds", true, + Containers::array({"", + "--magnum-disable-workarounds", "swiftshader-image-copy-extent-instead-of-layers"}), + "Device: {}\n" + "Device version: Vulkan {}.{}{}\n"} +}; + DeviceVkTest::DeviceVkTest(): VulkanTester{NoCreate} { addTests({&DeviceVkTest::createInfoConstruct, &DeviceVkTest::createInfoConstructNoImplicitExtensions, @@ -180,6 +204,9 @@ DeviceVkTest::DeviceVkTest(): VulkanTester{NoCreate} { &DeviceVkTest::constructExtensionsCommandLineEnable}, Containers::arraySize(ConstructCommandLineData)); + addInstancedTests({&DeviceVkTest::constructWorkaroundsCommandLineDisable}, + Containers::arraySize(ConstructWorkaroundsCommandLineData)); + addTests({&DeviceVkTest::constructMultipleQueues, &DeviceVkTest::constructRawQueue, &DeviceVkTest::constructFeatureNotSupported, @@ -750,7 +777,10 @@ void DeviceVkTest::constructExtensionsCommandLineDisable() { UnsignedInt minor = versionMinor(deviceProperties.version()); UnsignedInt patch = versionPatch(deviceProperties.version()); /* SwiftShader reports just 1.1 with no patch version, special-case that */ - CORRADE_COMPARE(out.str(), Utility::formatString(data.log, deviceProperties.name(), major, minor, patch ? Utility::formatString(".{}", patch) : "")); + std::string expected = Utility::formatString(data.log, deviceProperties.name(), major, minor, patch ? Utility::formatString(".{}", patch) : ""); + /* The output might contain a device workaround list, cut that away. + That's tested thoroughly in constructWorkaroundsCommandLineDisable(). */ + CORRADE_COMPARE(out.str().substr(0, expected.size()), expected); /* Verify that the entrypoint is actually (not) loaded as expected, to avoid all the above reporting being just smoke & mirrors */ @@ -804,7 +834,10 @@ void DeviceVkTest::constructExtensionsCommandLineEnable() { UnsignedInt minor = versionMinor(deviceProperties.version()); UnsignedInt patch = versionPatch(deviceProperties.version()); /* SwiftShader reports just 1.1 with no patch version, special-case that */ - CORRADE_COMPARE(out.str(), Utility::formatString(data.log, deviceProperties.name(), major, minor, patch ? Utility::formatString(".{}", patch) : "")); + std::string expected = Utility::formatString(data.log, deviceProperties.name(), major, minor, patch ? Utility::formatString(".{}", patch) : ""); + /* The output might contain a device workaround list, cut that away. + That's tested thoroughly in constructWorkaroundsCommandLineDisable(). */ + CORRADE_COMPARE(out.str().substr(0, expected.size()), expected); /* Verify that the entrypoint is actually (not) loaded as expected, to avoid all the above reporting being just smoke & mirrors */ @@ -812,6 +845,38 @@ void DeviceVkTest::constructExtensionsCommandLineEnable() { CORRADE_COMPARE(!!device->TrimCommandPoolKHR, data.maintenance1Enabled); } +void DeviceVkTest::constructWorkaroundsCommandLineDisable() { + auto&& data = ConstructWorkaroundsCommandLineData[testCaseInstanceId()]; + setTestCaseDescription(data.name); + + if(std::getenv("MAGNUM_VULKAN_VERSION")) + CORRADE_SKIP("Can't test with the MAGNUM_VULKAN_VERSION environment variable set"); + + /* Creating a dedicated instance so we can pass custom args */ + Instance instance2{InstanceCreateInfo{Int(data.args.size()), data.args}}; + + DeviceProperties deviceProperties = pickDevice(instance2); + + if(!deviceProperties.name().hasPrefix("SwiftShader"_s) && !data.shouldPassAlways) + CORRADE_SKIP("Workarounds only available on SwiftShader, can't test."); + + std::ostringstream out; + Debug redirectOutput{&out}; + Queue queue{NoCreate}; + Device device{instance2, DeviceCreateInfo{deviceProperties, DeviceCreateInfo::Flag::NoImplicitExtensions} + .addQueues(0, {0.0f}, {queue}) + }; + + CORRADE_VERIFY(device.handle()); + + /** @todo cleanup when Debug::toString() or some similar utility exists */ + UnsignedInt major = versionMajor(deviceProperties.version()); + UnsignedInt minor = versionMinor(deviceProperties.version()); + UnsignedInt patch = versionPatch(deviceProperties.version()); + /* SwiftShader reports just 1.1 with no patch version, special-case that */ + CORRADE_COMPARE(out.str(), Utility::formatString(data.log, deviceProperties.name(), major, minor, patch ? Utility::formatString(".{}", patch) : "")); +} + void DeviceVkTest::constructMultipleQueues() { /* Find a GPU that has at least two queue families and at least four queues in one family */ diff --git a/src/Magnum/Vk/Test/ImageVkTest.cpp b/src/Magnum/Vk/Test/ImageVkTest.cpp index 2f2476721..ae6a2274f 100644 --- a/src/Magnum/Vk/Test/ImageVkTest.cpp +++ b/src/Magnum/Vk/Test/ImageVkTest.cpp @@ -79,6 +79,7 @@ struct ImageVkTest: VulkanTester { void cmdClearStencilImage(); void cmdCopyImage2D(); + void cmdCopyImage2DArrayTo3D(); void cmdCopyImageDisallowedConversion(); void cmdCopyBufferImage1D(); @@ -120,6 +121,7 @@ ImageVkTest::ImageVkTest() { &ImageVkTest::cmdClearStencilImage, &ImageVkTest::cmdCopyImage2D, + &ImageVkTest::cmdCopyImage2DArrayTo3D, &ImageVkTest::cmdCopyImageDisallowedConversion, &ImageVkTest::cmdCopyBufferImage1D, @@ -672,6 +674,95 @@ void ImageVkTest::cmdCopyImage2D() { "----DdddDdddDdddDddd------------"_s); } +void ImageVkTest::cmdCopyImage2DArrayTo3D() { + CommandPool pool{device(), CommandPoolCreateInfo{ + device().properties().pickQueueFamily(QueueFlag::Graphics)}}; + CommandBuffer cmd = pool.allocate(); + + /* Mostly just to test the assertions in the + swiftshader-image-copy-extent-instead-of-layers workaround, but also if + I actually understand the overcomplicated ambiguous parameters + correctly. Apparently array/3D images can't be linear on SwiftShader, + so going through a buffer instead. */ + + /* Source buffer */ + Buffer a{device(), BufferCreateInfo{ + BufferUsage::TransferSource, 8*4*2*4 + }, MemoryFlag::HostVisible}; + Utility::copy("________________________________" + "________________________________" + + "____________AaaaAaaaAaaaAaaa____" + "____________BbbbBbbbBbbbBbbb____" + + "____________CcccCcccCcccCccc____" + "____________DdddDdddDdddDddd____" + + "________________________________" + "________________________________"_s, a.dedicatedMemory().map()); + + /* Source 2D array image, created from the buffer */ + Image b{device(), ImageCreateInfo2DArray{ + ImageUsage::TransferDestination|ImageUsage::TransferSource, + PixelFormat::RGBA8UI, {8, 2, 4}, 1 + }, MemoryFlag::DeviceLocal}; + + /* Destination 3D image, copied to a destination buffer */ + Image c{device(), ImageCreateInfo3D{ + ImageUsage::TransferDestination|ImageUsage::TransferSource, + PixelFormat::RGBA8UI, {8, 4, 2}, 1 + }, MemoryFlag::DeviceLocal}; + + /* Destination buffer */ + Buffer d{device(), BufferCreateInfo{ + BufferUsage::TransferDestination, 8*4*2*4 + }, MemoryFlag::HostVisible}; + + cmd.begin() + .pipelineBarrier(PipelineStage::TopOfPipe, PipelineStage::Transfer, { + {Accesses{}, Access::TransferWrite, + ImageLayout::Undefined, ImageLayout::TransferDestination, b}, + {Accesses{}, Access::TransferWrite, + ImageLayout::Undefined, ImageLayout::TransferDestination, c} + }) + .copyBufferToImage({a, b, ImageLayout::TransferDestination, { + BufferImageCopy2DArray{0, ImageAspect::Color, 0, {{}, {8, 2, 4}}} + }}) + .clearColorImage(c, ImageLayout::TransferDestination, Vector4ui{'-'}) + .pipelineBarrier(PipelineStage::Transfer, PipelineStage::Transfer, { + {Access::TransferWrite, Access::TransferRead, + ImageLayout::TransferDestination, ImageLayout::TransferSource, b}, + {Access::TransferWrite, Access::TransferWrite, + ImageLayout::TransferDestination, ImageLayout::TransferDestination, c} + }) + .copyImage({b, ImageLayout::TransferSource, c, ImageLayout::TransferDestination, { + {ImageAspect::Color, 0, 1, 2, {3, 0, 0}, 0, 0, 1, {1, 1, 0}, {4, 2, 2}} + }}) + .pipelineBarrier(PipelineStage::Transfer, PipelineStage::Transfer, { + {Access::TransferWrite, Access::TransferRead, + ImageLayout::TransferDestination, ImageLayout::TransferSource, c} + }) + .copyImageToBuffer({c, ImageLayout::TransferSource, d, { + BufferImageCopy3D{0, ImageAspect::Color, 0, {{}, {8, 4, 2}}} + }}) + .pipelineBarrier(PipelineStage::Transfer, PipelineStage::Host, { + {Access::TransferWrite, Access::HostRead, d} + }) + .end(); + queue().submit({SubmitInfo{}.setCommandBuffers({cmd})}).wait(); + + CORRADE_COMPARE(arrayView(d.dedicatedMemory().mapRead()), + "--------------------------------" + "----AaaaAaaaAaaaAaaa------------" + "----BbbbBbbbBbbbBbbb------------" + "--------------------------------" + + "--------------------------------" + "----CcccCcccCcccCccc------------" + "----DdddDdddDdddDddd------------" + "--------------------------------"_s); +} + void ImageVkTest::cmdCopyImageDisallowedConversion() { #ifdef CORRADE_NO_ASSERT CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");