KhronosGroup / MoltenVK

MoltenVK is a Vulkan Portability implementation. It layers a subset of the high-performance, industry-standard Vulkan graphics and compute API over Apple's Metal graphics framework, enabling Vulkan applications to run on macOS, iOS and tvOS.
Apache License 2.0
4.76k stars 419 forks source link

ImGui flickering when use mutable swapchain format for two dynamic renderings. #2261

Open stripe2933 opened 3 months ago

stripe2933 commented 3 months ago

I'm not sure if this error is related to ImGui or MoltenVK. However, since this flickering is not observed when testing the same code in a Windows environment with NVIDIA drivers, I am raising this issue with MoltenVK.

ImGui does not support proper gamma correction for the B8G8R8A8_SRGB format. Therefore, I am using the VK_KHR_mutable_swapchain_format extension and specifying the two formats, B8G8R8A8_SRGB and B8G8R8A8_UNORM, in VkImageFormatListCreateInfo in the pNext of VkSwapchainCreateInfoKHR. During ImGui rendering, I render to the swapchain image view using the B8G8R8A8_UNORM format. Here is some of the code I am using:

auto createSwapchain(
    vk::SwapchainKHR oldSwapchain
) -> vk::raii::SwapchainKHR {
    const vk::SurfaceCapabilitiesKHR surfaceCapabilities = gpu.physicalDevice.getSurfaceCapabilitiesKHR(surface);
    return { gpu.device, vk::StructureChain {
        vk::SwapchainCreateInfoKHR {
            vk::SwapchainCreateFlagBitsKHR::eMutableFormat,
            surface,
            std::min(surfaceCapabilities.minImageCount + 1, surfaceCapabilities.maxImageCount),
            vk::Format::eB8G8R8A8Srgb,
            vk::ColorSpaceKHR::eSrgbNonlinear,
            (swapchainImageExtent = surfaceCapabilities.currentExtent),
            1,
            vk::ImageUsageFlagBits::eColorAttachment,
            vk::SharingMode::eExclusive, {},
            surfaceCapabilities.currentTransform,
            vk::CompositeAlphaFlagBitsKHR::eOpaque,
            vk::PresentModeKHR::eFifo,
            {},
            oldSwapchain,
        },
        vk::ImageFormatListCreateInfo {
            unsafeProxy({
                vk::Format::eB8G8R8A8Srgb,
                vk::Format::eB8G8R8A8Unorm,
            })
        },
    }.get() };
}

// Begin dynamic rendering with B8G8R8A8_SRGB format. cb.beginRenderingKHR(vk::RenderingInfo { {}, { { 0, 0 }, sharedData.swapchainImageExtent }, 1, 0, unsafeProxy({ vk::RenderingAttachmentInfo { sharedData.swapchainImageViews[swapchainImageIndex] / format=B8G8R8A8_SRGB */, vk::ImageLayout::eColorAttachmentOptimal, {}, {}, {}, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::ClearColorValue { 0.f, 0.f, 0.f, 1.f }, }, }), });

cb.setViewport(0, vk::Viewport { 0.f, 0.f, static_cast(sharedData.swapchainImageExtent.width), static_cast(sharedData.swapchainImageExtent.height), 0.f, 1.f }); cb.setScissor(0, vk::Rect2D { { 0, 0 }, sharedData.swapchainImageExtent });

// Draw a simple triangle. cb.bindPipeline(vk::PipelineBindPoint::eGraphics, *sharedData.triangleRenderer.pipeline); cb.draw(3, 1, 0, 0);

cb.endRenderingKHR();

// Memory barrier that ensure the triangle rendering pass is done before imgui do. cb.pipelineBarrier( vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eColorAttachmentOutput, {}, {}, {}, vk::ImageMemoryBarrier { vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eColorAttachmentRead, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::eColorAttachmentOptimal, vk::QueueFamilyIgnored, vk::QueueFamilyIgnored, sharedData.swapchainImages[swapchainImageIndex], { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 }, });

// Begin dynamic rendering with B8G8R8A8_UNORM format. cb.beginRenderingKHR(vk::RenderingInfo { {}, { { 0, 0 }, sharedData.swapchainImageExtent }, 1, 0, unsafeProxy({ vk::RenderingAttachmentInfo { sharedData.imGuiSwapchainImageViews[swapchainImageIndex] / format=B8G8R8A8_UNORM /, vk::ImageLayout::eColorAttachmentOptimal, {}, {}, {}, vk::AttachmentLoadOp::eLoad / previously rendered image must not be cleared */, vk::AttachmentStoreOp::eStore, vk::ClearColorValue { 0.f, 0.f, 0.f, 1.f }, }, }), });

// Draw ImGui data. ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cb);

cb.endRenderingKHR();

// Change the current swapchain image layout from COLOR_ATTACHMENT_OPTIMAL to PRESENT_SRC_KHR. cb.pipelineBarrier( vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eBottomOfPipe, {}, {}, {}, vk::ImageMemoryBarrier { vk::AccessFlagBits::eColorAttachmentWrite, {}, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::ePresentSrcKHR, vk::QueueFamilyIgnored, vk::QueueFamilyIgnored, sharedData.swapchainImages[swapchainImageIndex], { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 }, });



When using this code, flickering occurs as shown in the video below. The validation layers, including synchronization validation, do not raise any issues.

One puzzling point is that if I do not use the mutable format for rendering (i.e., use `B8G8R8A8_SRGB` in the `PipelineRenderingCreateInfo` of `ImGui_ImplVulkan_InitInfo` and use `swapchainImageViews` instead of `imGuiSwapchainImageViews`), this flickering does not occur.

- With mutable format

https://github.com/KhronosGroup/MoltenVK/assets/63503910/956b8eba-da82-4ffe-85ec-40f604bbf4b8

- Without mutable format (flickering not occurred)

<img width="912" alt="Without mutable format" src="https://github.com/KhronosGroup/MoltenVK/assets/63503910/4f5a4e76-4904-47a7-9a17-a0409301a89b">
stripe2933 commented 3 months ago

https://github.com/KhronosGroup/MoltenVK/assets/63503910/abd593df-3be6-4267-b09f-0b2fbab7266a

Based on several experiments, it seems to be an issue with MoltenVK. When I replaced the ImGui rendering part of the code with another rectangle rendering code, the same flickering was observed. The flickering becomes more frequent as the frame time approaches the display refresh rate (1s/120=8.3ms).

One peculiar point that not shown in the above video is that the red rectangle in the video often hide the Metal HUD.

stripe2933 commented 2 months ago

Full reproducing code:

#include <cassert>
#include <cstdint>
#include <chrono>
#include <format>
#include <initializer_list>
#include <ranges>
#include <stdexcept>
#include <thread>
#include <vector>

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <vulkan/vulkan_raii.hpp>

#define USE_MUTABLE_FORMAT 1 // Set it to 0 for non-mutable format.
#define FRAME_SLEEP_TIME 1e-2f // As more as it reaches to the frame time (8 ms for 120 FPS), red triangle will be shown more.

template <typename T>
[[nodiscard]] auto unsafeAddress(const T &value [[clang::lifetimebound]]) noexcept -> const T* {
    return &value;
}

template <std::ranges::contiguous_range R>
[[nodiscard]] auto unsafeProxy(const R &range [[clang::lifetimebound]]) noexcept -> vk::ArrayProxyNoTemporaries<const std::ranges::range_value_t<R>> {
    return range;
}

template <typename T>
[[nodiscard]] auto unsafeProxy(const std::initializer_list<T> &list [[clang::lifetimebound]]) noexcept -> vk::ArrayProxyNoTemporaries<const T> {
    return list;
}

/*
    #version 460

    const vec2 positions[] = {
        { 0.8, -0.8 },
        { -0.8, 0.8 },
        { 0.8, 0.8 },
    };

    void main(){
        gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
    }
*/
constexpr std::uint32_t triangle_vert_spv[] = {
  0x03022307U, 0x00000100U, 0x0b000d00U,
  0x28000000U, 0x00000000U, 0x11000200U,
  0x01000000U, 0x0b000600U, 0x01000000U,
  0x474c534cU, 0x2e737464U, 0x2e343530U,
  0x00000000U, 0x0e000300U, 0x00000000U,
  0x01000000U, 0x0f000700U, 0x00000000U,
  0x04000000U, 0x6d61696eU, 0x00000000U,
  0x0d000000U, 0x1a000000U, 0x03000300U,
  0x02000000U, 0xcc010000U, 0x04000a00U,
  0x474c5f47U, 0x4f4f474cU, 0x455f6370U,
  0x705f7374U, 0x796c655fU, 0x6c696e65U,
  0x5f646972U, 0x65637469U, 0x76650000U,
  0x04000800U, 0x474c5f47U, 0x4f4f474cU,
  0x455f696eU, 0x636c7564U, 0x655f6469U,
  0x72656374U, 0x69766500U, 0x05000400U,
  0x04000000U, 0x6d61696eU, 0x00000000U,
  0x05000600U, 0x0b000000U, 0x676c5f50U,
  0x65725665U, 0x72746578U, 0x00000000U,
  0x06000600U, 0x0b000000U, 0x00000000U,
  0x676c5f50U, 0x6f736974U, 0x696f6e00U,
  0x06000700U, 0x0b000000U, 0x01000000U,
  0x676c5f50U, 0x6f696e74U, 0x53697a65U,
  0x00000000U, 0x06000700U, 0x0b000000U,
  0x02000000U, 0x676c5f43U, 0x6c697044U,
  0x69737461U, 0x6e636500U, 0x06000700U,
  0x0b000000U, 0x03000000U, 0x676c5f43U,
  0x756c6c44U, 0x69737461U, 0x6e636500U,
  0x05000300U, 0x0d000000U, 0x00000000U,
  0x05000600U, 0x1a000000U, 0x676c5f56U,
  0x65727465U, 0x78496e64U, 0x65780000U,
  0x05000500U, 0x1d000000U, 0x696e6465U,
  0x7861626cU, 0x65000000U, 0x48000500U,
  0x0b000000U, 0x00000000U, 0x0b000000U,
  0x00000000U, 0x48000500U, 0x0b000000U,
  0x01000000U, 0x0b000000U, 0x01000000U,
  0x48000500U, 0x0b000000U, 0x02000000U,
  0x0b000000U, 0x03000000U, 0x48000500U,
  0x0b000000U, 0x03000000U, 0x0b000000U,
  0x04000000U, 0x47000300U, 0x0b000000U,
  0x02000000U, 0x47000400U, 0x1a000000U,
  0x0b000000U, 0x2a000000U, 0x13000200U,
  0x02000000U, 0x21000300U, 0x03000000U,
  0x02000000U, 0x16000300U, 0x06000000U,
  0x20000000U, 0x17000400U, 0x07000000U,
  0x06000000U, 0x04000000U, 0x15000400U,
  0x08000000U, 0x20000000U, 0x00000000U,
  0x2b000400U, 0x08000000U, 0x09000000U,
  0x01000000U, 0x1c000400U, 0x0a000000U,
  0x06000000U, 0x09000000U, 0x1e000600U,
  0x0b000000U, 0x07000000U, 0x06000000U,
  0x0a000000U, 0x0a000000U, 0x20000400U,
  0x0c000000U, 0x03000000U, 0x0b000000U,
  0x3b000400U, 0x0c000000U, 0x0d000000U,
  0x03000000U, 0x15000400U, 0x0e000000U,
  0x20000000U, 0x01000000U, 0x2b000400U,
  0x0e000000U, 0x0f000000U, 0x00000000U,
  0x17000400U, 0x10000000U, 0x06000000U,
  0x02000000U, 0x2b000400U, 0x08000000U,
  0x11000000U, 0x03000000U, 0x1c000400U,
  0x12000000U, 0x10000000U, 0x11000000U,
  0x2b000400U, 0x06000000U, 0x13000000U,
  0xcdcc4c3fU, 0x2b000400U, 0x06000000U,
  0x14000000U, 0xcdcc4cbfU, 0x2c000500U,
  0x10000000U, 0x15000000U, 0x13000000U,
  0x14000000U, 0x2c000500U, 0x10000000U,
  0x16000000U, 0x14000000U, 0x13000000U,
  0x2c000500U, 0x10000000U, 0x17000000U,
  0x13000000U, 0x13000000U, 0x2c000600U,
  0x12000000U, 0x18000000U, 0x15000000U,
  0x16000000U, 0x17000000U, 0x20000400U,
  0x19000000U, 0x01000000U, 0x0e000000U,
  0x3b000400U, 0x19000000U, 0x1a000000U,
  0x01000000U, 0x20000400U, 0x1c000000U,
  0x07000000U, 0x12000000U, 0x20000400U,
  0x1e000000U, 0x07000000U, 0x10000000U,
  0x2b000400U, 0x06000000U, 0x21000000U,
  0x00000000U, 0x2b000400U, 0x06000000U,
  0x22000000U, 0x0000803fU, 0x20000400U,
  0x26000000U, 0x03000000U, 0x07000000U,
  0x36000500U, 0x02000000U, 0x04000000U,
  0x00000000U, 0x03000000U, 0xf8000200U,
  0x05000000U, 0x3b000400U, 0x1c000000U,
  0x1d000000U, 0x07000000U, 0x3d000400U,
  0x0e000000U, 0x1b000000U, 0x1a000000U,
  0x3e000300U, 0x1d000000U, 0x18000000U,
  0x41000500U, 0x1e000000U, 0x1f000000U,
  0x1d000000U, 0x1b000000U, 0x3d000400U,
  0x10000000U, 0x20000000U, 0x1f000000U,
  0x51000500U, 0x06000000U, 0x23000000U,
  0x20000000U, 0x00000000U, 0x51000500U,
  0x06000000U, 0x24000000U, 0x20000000U,
  0x01000000U, 0x50000700U, 0x07000000U,
  0x25000000U, 0x23000000U, 0x24000000U,
  0x21000000U, 0x22000000U, 0x41000500U,
  0x26000000U, 0x27000000U, 0x0d000000U,
  0x0f000000U, 0x3e000300U, 0x27000000U,
  0x25000000U, 0xfd000100U, 0x38000100U
};

/*
    #version 460

    layout (location = 0) out vec4 outColor;

    layout (push_constant) uniform PushConstant {
        vec3 color;
    };

    void main(){
        outColor = vec4(color, 1.0);
    }
*/
constexpr std::uint32_t triangle_frag_spv[] = {
  0x03022307U, 0x00000100U, 0x0b000d00U,
  0x18000000U, 0x00000000U, 0x11000200U,
  0x01000000U, 0x0b000600U, 0x01000000U,
  0x474c534cU, 0x2e737464U, 0x2e343530U,
  0x00000000U, 0x0e000300U, 0x00000000U,
  0x01000000U, 0x0f000600U, 0x04000000U,
  0x04000000U, 0x6d61696eU, 0x00000000U,
  0x09000000U, 0x10000300U, 0x04000000U,
  0x07000000U, 0x03000300U, 0x02000000U,
  0xcc010000U, 0x04000a00U, 0x474c5f47U,
  0x4f4f474cU, 0x455f6370U, 0x705f7374U,
  0x796c655fU, 0x6c696e65U, 0x5f646972U,
  0x65637469U, 0x76650000U, 0x04000800U,
  0x474c5f47U, 0x4f4f474cU, 0x455f696eU,
  0x636c7564U, 0x655f6469U, 0x72656374U,
  0x69766500U, 0x05000400U, 0x04000000U,
  0x6d61696eU, 0x00000000U, 0x05000500U,
  0x09000000U, 0x6f757443U, 0x6f6c6f72U,
  0x00000000U, 0x05000600U, 0x0b000000U,
  0x50757368U, 0x436f6e73U, 0x74616e74U,
  0x00000000U, 0x06000500U, 0x0b000000U,
  0x00000000U, 0x636f6c6fU, 0x72000000U,
  0x05000300U, 0x0d000000U, 0x00000000U,
  0x47000400U, 0x09000000U, 0x1e000000U,
  0x00000000U, 0x48000500U, 0x0b000000U,
  0x00000000U, 0x23000000U, 0x00000000U,
  0x47000300U, 0x0b000000U, 0x02000000U,
  0x13000200U, 0x02000000U, 0x21000300U,
  0x03000000U, 0x02000000U, 0x16000300U,
  0x06000000U, 0x20000000U, 0x17000400U,
  0x07000000U, 0x06000000U, 0x04000000U,
  0x20000400U, 0x08000000U, 0x03000000U,
  0x07000000U, 0x3b000400U, 0x08000000U,
  0x09000000U, 0x03000000U, 0x17000400U,
  0x0a000000U, 0x06000000U, 0x03000000U,
  0x1e000300U, 0x0b000000U, 0x0a000000U,
  0x20000400U, 0x0c000000U, 0x09000000U,
  0x0b000000U, 0x3b000400U, 0x0c000000U,
  0x0d000000U, 0x09000000U, 0x15000400U,
  0x0e000000U, 0x20000000U, 0x01000000U,
  0x2b000400U, 0x0e000000U, 0x0f000000U,
  0x00000000U, 0x20000400U, 0x10000000U,
  0x09000000U, 0x0a000000U, 0x2b000400U,
  0x06000000U, 0x13000000U, 0x0000803fU,
  0x36000500U, 0x02000000U, 0x04000000U,
  0x00000000U, 0x03000000U, 0xf8000200U,
  0x05000000U, 0x41000500U, 0x10000000U,
  0x11000000U, 0x0d000000U, 0x0f000000U,
  0x3d000400U, 0x0a000000U, 0x12000000U,
  0x11000000U, 0x51000500U, 0x06000000U,
  0x14000000U, 0x12000000U, 0x00000000U,
  0x51000500U, 0x06000000U, 0x15000000U,
  0x12000000U, 0x01000000U, 0x51000500U,
  0x06000000U, 0x16000000U, 0x12000000U,
  0x02000000U, 0x50000700U, 0x07000000U,
  0x17000000U, 0x14000000U, 0x15000000U,
  0x16000000U, 0x13000000U, 0x3e000300U,
  0x09000000U, 0x17000000U, 0xfd000100U,
  0x38000100U
};

struct TriangleRenderer {
    vk::raii::PipelineLayout pipelineLayout;
    vk::raii::Pipeline pipeline;

    TriangleRenderer(const vk::raii::Device &device, vk::Format format)
        : pipelineLayout { device, vk::PipelineLayoutCreateInfo {
            {},
            {},
            unsafeProxy({
                vk::PushConstantRange {
                    vk::ShaderStageFlagBits::eFragment,
                    0, sizeof(float) * 3,
                },
            })
        } }
        , pipeline { device, nullptr, vk::StructureChain {
            vk::GraphicsPipelineCreateInfo {
                {},
                unsafeProxy({
                    vk::PipelineShaderStageCreateInfo {
                        {},
                        vk::ShaderStageFlagBits::eVertex,
                        *vk::raii::ShaderModule { device, vk::ShaderModuleCreateInfo {
                            {},
                            triangle_vert_spv,
                        } },
                        "main",
                    },
                    vk::PipelineShaderStageCreateInfo {
                        {},
                        vk::ShaderStageFlagBits::eFragment,
                        *vk::raii::ShaderModule { device, vk::ShaderModuleCreateInfo {
                            {},
                            triangle_frag_spv,
                        } },
                        "main",
                    },
                }),
                unsafeAddress(vk::PipelineVertexInputStateCreateInfo{}),
                unsafeAddress(vk::PipelineInputAssemblyStateCreateInfo {
                    {},
                    vk::PrimitiveTopology::eTriangleList,
                }),
                {},
                unsafeAddress(vk::PipelineViewportStateCreateInfo {
                    {},
                    1, nullptr,
                    1, nullptr,
                }),
                unsafeAddress(vk::PipelineRasterizationStateCreateInfo {
                    {},
                    false, false,
                    vk::PolygonMode::eFill,
                    vk::CullModeFlagBits::eNone, {},
                    {}, {}, {}, {},
                    1.f,
                }),
                unsafeAddress(vk::PipelineMultisampleStateCreateInfo {
                    {},
                    vk::SampleCountFlagBits::e1,
                }),
                {},
                unsafeAddress(vk::PipelineColorBlendStateCreateInfo {
                    {},
                    false, {},
                    unsafeProxy({
                        vk::PipelineColorBlendAttachmentState {
                            false,
                            {}, {}, {},
                            {}, {}, {},
                            vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
                        },
                    }),
                }),
                unsafeAddress(vk::PipelineDynamicStateCreateInfo {
                    {},
                    unsafeProxy({
                        vk::DynamicState::eViewport,
                        vk::DynamicState::eScissor,
                    }),
                }),
                *pipelineLayout,
            },
            vk::PipelineRenderingCreateInfo {
                {},
                unsafeProxy({ format }),
            },
        }.get() } { }
};

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    GLFWwindow* const window = glfwCreateWindow(512, 512, "Vulkan Window", nullptr, nullptr);

    const vk::raii::Context context;

    const vk::raii::Instance instance { context, vk::InstanceCreateInfo {
        vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
        unsafeAddress(vk::ApplicationInfo {
            "MoltenVK mutable format error", 0,
            {}, 0,
            vk::makeApiVersion(0, 1, 2, 0), // MoltenVK conforms Vulkan 1.2.
        }),
        unsafeProxy({
            "VK_LAYER_KHRONOS_validation",
        }),
        unsafeProxy([]() -> std::vector<const char*> {
            std::vector extensions {
                vk::KHRPortabilityEnumerationExtensionName,
            };

            // Add Vulkan extensions for GLFW.
            std::uint32_t glfwExtensionCount;
            extensions.append_range(std::views::counted(glfwGetRequiredInstanceExtensions(&glfwExtensionCount), glfwExtensionCount));

            return extensions;
        }())
    } };

    const vk::raii::SurfaceKHR surface = [&]() -> vk::raii::SurfaceKHR {
        if (VkSurfaceKHR surface; glfwCreateWindowSurface(*instance, window, nullptr, &surface) == VK_SUCCESS) {
            return { instance, surface };
        }

        const char *errorMessage;
        const int errorCode = glfwGetError(&errorMessage);
        throw std::runtime_error { std::format("Failed to create GLFW window surface: {} (error code={})", errorMessage, errorCode) };
    }();

    // Use first physical device for simplification.
    const vk::raii::PhysicalDevice physicalDevice = instance.enumeratePhysicalDevices().front();

    const vk::raii::Device device { physicalDevice, vk::StructureChain {
        vk::DeviceCreateInfo {
            {},
            unsafeProxy({
                vk::DeviceQueueCreateInfo {
                    {},
                    0, // Assume first queue family supports both graphics and present operation.
                    unsafeProxy({ 1.f }),
                },
            }),
            {},
            unsafeProxy({
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
                vk::KHRDynamicRenderingExtensionName,
#pragma clang diagnostic pop
                vk::KHRPortabilitySubsetExtensionName,
                vk::KHRSwapchainExtensionName,
#if USE_MUTABLE_FORMAT
                vk::KHRSwapchainMutableFormatExtensionName,
#endif
            }),
        },
        vk::PhysicalDeviceDynamicRenderingFeatures { true },
    }.get() };

    const vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface);
    const vk::raii::SwapchainKHR swapchain { device, vk::StructureChain {
        vk::SwapchainCreateInfoKHR {
#if USE_MUTABLE_FORMAT
            vk::SwapchainCreateFlagBitsKHR::eMutableFormat,
#else
            {},
#endif
            *surface,
            std::min(surfaceCapabilities.minImageCount + 1, surfaceCapabilities.maxImageCount),
            vk::Format::eB8G8R8A8Srgb,
            vk::ColorSpaceKHR::eSrgbNonlinear,
            surfaceCapabilities.currentExtent,
            1,
            vk::ImageUsageFlagBits::eColorAttachment,
            vk::SharingMode::eExclusive, {},
            surfaceCapabilities.currentTransform,
            vk::CompositeAlphaFlagBitsKHR::eOpaque,
            vk::PresentModeKHR::eFifo,
        },
#if USE_MUTABLE_FORMAT
        vk::ImageFormatListCreateInfo {
            unsafeProxy({
                vk::Format::eB8G8R8A8Srgb,
                vk::Format::eB8G8R8A8Unorm,
            }),
        },
#endif
    }.get() };

    const std::vector swapchainImages = (*device).getSwapchainImagesKHR(*swapchain);

    const auto createSwapchainImageViews = [&](vk::Format format) -> std::vector<vk::raii::ImageView> {
        return swapchainImages
            | std::views::transform([&](vk::Image image) {
                return vk::raii::ImageView { device, vk::ImageViewCreateInfo {
                    {},
                    image,
                    vk::ImageViewType::e2D,
                    format,
                    {},
                    { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 },
                } };
            })
            | std::ranges::to<std::vector>();
    };
    const std::vector swapchainSrgbImageViews = createSwapchainImageViews(vk::Format::eB8G8R8A8Srgb);
#if USE_MUTABLE_FORMAT
    const std::vector swapchainUnormImageViews = createSwapchainImageViews(vk::Format::eB8G8R8A8Unorm);
#endif

    const TriangleRenderer triangleSrgbRenderer { device, vk::Format::eB8G8R8A8Srgb };
#if USE_MUTABLE_FORMAT
    const TriangleRenderer triangleUnormRenderer { device, vk::Format::eB8G8R8A8Unorm };
#endif

    const vk::raii::CommandPool commandPool { device, vk::CommandPoolCreateInfo {
        vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
        0, // Assume first queue family supports graphics operation.
    } };
    const vk::CommandBuffer commandBuffer = (*device).allocateCommandBuffers(vk::CommandBufferAllocateInfo {
        *commandPool,
        vk::CommandBufferLevel::ePrimary,
        1,
    })[0];

    const vk::Queue queue = *device.getQueue(0, 0);

    const vk::raii::Semaphore swapchainImageAvailableSemaphore { device, vk::SemaphoreCreateInfo{} };
    const vk::raii::Semaphore drawFinishSemaphore { device, vk::SemaphoreCreateInfo{} };
    const vk::raii::Fence frameFinishFence { device, vk::FenceCreateInfo { vk::FenceCreateFlagBits::eSignaled } };

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();

        const vk::Result frameFinishResult = device.waitForFences(*frameFinishFence, true, ~0U);
        assert(frameFinishResult == vk::Result::eSuccess && "Failed to wait for frame finish fence.");
        device.resetFences(*frameFinishFence);

        using namespace std::chrono_literals;
        std::this_thread::sleep_for(FRAME_SLEEP_TIME * 1s);

        const auto [swapchainImageAvailableResult, swapchainImageIndex] = (*device).acquireNextImageKHR(*swapchain, ~0U, *swapchainImageAvailableSemaphore, {});
        assert(swapchainImageAvailableResult == vk::Result::eSuccess && "Failed to acquire next swapchain image.");

        commandPool.reset();
        commandBuffer.begin({ vk::CommandBufferUsageFlagBits::eOneTimeSubmit });

        // --------------------
        // Change swapchain image layout for rendering.
        // --------------------

        commandBuffer.pipelineBarrier(
            vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eColorAttachmentOutput,
            {}, {}, {},
            vk::ImageMemoryBarrier {
                {}, vk::AccessFlagBits::eColorAttachmentWrite,
                {}, vk::ImageLayout::eColorAttachmentOptimal,
                vk::QueueFamilyIgnored, vk::QueueFamilyIgnored,
                swapchainImages[swapchainImageIndex],
                { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 },
            });

        commandBuffer.setViewport(0, vk::Viewport {
            0.f, 0.f,
            static_cast<float>(surfaceCapabilities.currentExtent.width), static_cast<float>(surfaceCapabilities.currentExtent.height),
            0.f, 1.f,
        });
        commandBuffer.setScissor(0, vk::Rect2D {
            { 0, 0 },
            surfaceCapabilities.currentExtent,
        });

        // --------------------
        // Draw red triangle.
        // --------------------

        commandBuffer.beginRenderingKHR({
            {},
            vk::Rect2D { { 0, 0 }, surfaceCapabilities.currentExtent },
            1,
            0,
            unsafeProxy({
                vk::RenderingAttachmentInfo {
                    swapchainSrgbImageViews[swapchainImageIndex], vk::ImageLayout::eColorAttachmentOptimal,
                    {}, {}, {},
                    vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::ClearColorValue { 0.f, 0.f, 0.f, 1.f },
                },
            }),
        }, *device.getDispatcher());

        commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *triangleSrgbRenderer.pipeline);
        commandBuffer.pushConstants(*triangleSrgbRenderer.pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(float) * 3, unsafeAddress(std::array { 1.f, 0.f, 0.f }));
        commandBuffer.draw(3, 1, 0, 0);

        commandBuffer.endRenderingKHR(*device.getDispatcher());

        // --------------------
        // Make sure the red triangle is drawn before the blue triangle.
        // --------------------

        commandBuffer.pipelineBarrier(
            vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eColorAttachmentOutput,
            {},
            vk::MemoryBarrier { vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eColorAttachmentRead },
            {}, {});

        // --------------------
        // Draw blue triangle. It must hide the red triangle.
        // --------------------

        commandBuffer.beginRenderingKHR({
            {},
            vk::Rect2D { { 0, 0 }, surfaceCapabilities.currentExtent },
            1,
            0,
            unsafeProxy({
                vk::RenderingAttachmentInfo {
#if USE_MUTABLE_FORMAT
                    swapchainUnormImageViews[swapchainImageIndex], vk::ImageLayout::eColorAttachmentOptimal,
#else
                    swapchainSrgbImageViews[swapchainImageIndex], vk::ImageLayout::eColorAttachmentOptimal,
#endif
                    {}, {}, {},
                    vk::AttachmentLoadOp::eLoad, vk::AttachmentStoreOp::eStore, vk::ClearColorValue{},
                },
            }),
        }, *device.getDispatcher());

#if USE_MUTABLE_FORMAT
        commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *triangleUnormRenderer.pipeline);
        commandBuffer.pushConstants(*triangleUnormRenderer.pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(float) * 3, unsafeAddress(std::array { 0.f, 0.f, 1.f }));
#else
        commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *triangleSrgbRenderer.pipeline);
        commandBuffer.pushConstants(*triangleSrgbRenderer.pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(float) * 3, unsafeAddress(std::array { 0.f, 0.f, 1.f }));
#endif
        commandBuffer.draw(3, 1, 0, 0);

        commandBuffer.endRenderingKHR(*device.getDispatcher());

        // --------------------
        // Change swapchain image layout for presentation.
        // --------------------

        commandBuffer.pipelineBarrier(
            vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eBottomOfPipe,
            {}, {}, {},
            vk::ImageMemoryBarrier {
                vk::AccessFlagBits::eColorAttachmentWrite, {},
                vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::ePresentSrcKHR,
                vk::QueueFamilyIgnored, vk::QueueFamilyIgnored,
                swapchainImages[swapchainImageIndex],
                { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 },
            });

        commandBuffer.end();

        queue.submit(vk::SubmitInfo {
            *swapchainImageAvailableSemaphore,
            unsafeProxy({ vk::Flags { vk::PipelineStageFlagBits::eColorAttachmentOutput } }),
            commandBuffer,
            *drawFinishSemaphore,
        }, *frameFinishFence);

        const vk::Result swapchainImagePresentResult = queue.presentKHR(vk::PresentInfoKHR {
            *drawFinishSemaphore,
            *swapchain,
            swapchainImageIndex,
        });
        assert(swapchainImagePresentResult == vk::Result::eSuccess && "Failed to present swapchain image.");
    }

    device.waitIdle();

    glfwDestroyWindow(window);

    glfwTerminate();
}

Expected result: only blue triangle is shown (set USE_MUTABLE_FORMAT to 0) Actual result: red triangle shown up and it gets more flickering when increasing FRAME_SLEEP_TIME (set USE_MUTABLE_FORMAT to 1)