steps3D - Tutorials - Расширение VK_EXT_host_image_copy

Расширение VK_EXT_host_image_copy

При написании различных графических приложений в Vulkan постоянно возникает необходимость в загрузке текстур (изображений) из памяти CPU в память GPU. Если в OpenGL все было очень просто - мы просто передавали в команду glTexImage* указатель на текселы и вся работа делалась скрыто от нас не требуя какого-либо вмешательства, то с Vulkan все гораздо сложнее.

Обычно нам нужен вспомогательный буфер видимый CPU, через который и осуществляется копирование. Но кроме того, нужен командный буфер (а значит, и очередь куда он будет передаваться для выполнения) и синхронизация (копирование это асинхронная операция). Т.е. требуется целый набор действий как на стороне CPU, так и на стороне GPU.

И тут нам на помощь приходит расширение VK_EXT_host_image_copy, вошедшее в состав Vulkan 1.4. Оно позволяет осуществить загрузку всего изображения (включая слои mipmap-пирамиды) за одно обращение к Vulkan, задействуя при этом (с нашей стороны) только CPU. Никаких командных буферов, вспомогательный буферов и т.п. Таким образом использование данного расширения позволяет сделать загрузку текстур (изображений) в Vulkan заметно проще для программиста.

Но, как вы должно быть уже догадались, не все так просто :) Не все форматы изображений поддерживают подобный способ загрузки, синхронизация сама никуда не уходит и это по-прежнему обязанность программиста. Кроме того, подобная загрузке может быть дороже (т.е. медленнее) чем традиционная.

Итак, давайте рассмотрим как мы можем на практике использовать данное расширение. Первое, что мы должны сделать - это указать имя расширения, т.е. VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME (лучше использовать данное имя вместо "VK_EXT_host_image_copy" - в случае опечатки вы получите ошибку на этапе компиляции) в списке требуемых расширений устройства (или потребовать поддержку Vulkan 1.4). Кроме этого, необходимо через поле hostImageCopy структуры VkPhysicalDeviceHostImageCopyFeaturesEXT задать, что мы будем использовать данную возможность.

DevicePolicy    policy;

        // we require VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME device extension
policy.addDeviceExtension ( VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME  );

VkPhysicalDeviceHostImageCopyFeaturesEXT    hostCopyFeatures = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES_EXT };
hostCopyFeatures.hostImageCopy = VK_TRUE;

policy.addFeatures   ( &hostCopyFeatures );

Далее мы создаем изображение, в которое мы будем загружать текселы. После того, как изображение будет успешно создано нужно перевести его в нужный нам layout и загрузить все слои mipmap-пирамиды в него. Обратите внимание, что мы сразу переводим изображение в тот layout, в котором мы будем его использовать, и потом делаем загрузку данных - преобразование будет выполнено автоматически и командный буфер нам здесь совсем не нужен.

Расширение VK_EXT_host_image_copy вводит как команды для копирования данных между памятью CPU и изображением Vulkan, так и команду для перевода изображения из одного layout в другой без использования командных буферов.

VkResult vkCopyMemoryToImageEXT (
    VkDevice                                    device,
    const VkCopyMemoryToImageInfo*              pCopyMemoryToImageInfo );

typedef struct VkHostImageLayoutTransitionInfo {
    VkStructureType            sType;
    const void*                pNext;
    VkImage                    image;
    VkImageLayout              oldLayout;
    VkImageLayout              newLayout;
    VkImageSubresourceRange    subresourceRange;
} VkHostImageLayoutTransitionInfo;
 
VkResult vkTransitionImageLayoutEXT (
    VkDevice                                    device,
    uint32_t                                    transitionCount,
    const VkHostImageLayoutTransitionInfo*      pTransitions );

Самым простым из всего вводимого данным расширением является перевод изображения из одного layout в другой - мы просто заполняем поля структуры VkHostImagelayoutTransitionEXT и вызываем команду vkTransitionImageLayoutEXT:

VkHostImageLayoutTransitionInfoEXT host_image_layout_transition_info = {};

host_image_layout_transition_info.sType            = VK_STRUCTURE_TYPE_HOST_IMAGE_LAYOUT_TRANSITION_INFO_EXT;
host_image_layout_transition_info.image            = texture.getImage().getHandle ();
host_image_layout_transition_info.oldLayout        = VK_IMAGE_LAYOUT_UNDEFINED;
host_image_layout_transition_info.newLayout        = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
host_image_layout_transition_info.subresourceRange = subresourceRange;

vkTransitionImageLayoutEXT ( device.getDevice (), 1, &host_image_layout_transition_info );

Данные для копирования организованы чуть сложнее, зато за одну команду копирования мы можем скопировать сразу все слои mipmap-пирамиды для всех элементов текстурного массива. Ниже приводится пример, как за один вызов vkCopyMemoryToImageEXT можно загрузить все слои mipmap-пирамиды для простого изображения в формате VK_FORMAT_R8G8B8A8_UNORM. Кроме команды для копирования данных из памяти CPU в изображение Vulkan есть и обратная команда - копирование данных из изображения в память CPU - vkCopyImageToMemoryEXT. Ниже приводится ее описание.

typedef struct VkCopyImageToMemoryInfo {
    VkStructureType               sType;
    const void*                   pNext;
    VkHostImageCopyFlags          flags;
    VkImage                       srcImage;
    VkImageLayout                 srcImageLayout;
    uint32_t                      regionCount;
    const VkImageToMemoryCopy*    pRegions;
} VkCopyImageToMemoryInfo;
 
VkResult vkCopyImageToMemoryEXT (
    VkDevice                                    device,
    const VkCopyImageToMemoryInfo*              pCopyImageToMemoryInfo );

Обратите внимание, что поддержка данного расширения не гарантирует того, что все изображения могут быть скопированы со стороны CPU описанным выше способом. Кроме того, даже если поддержка есть, то для некоторых layout'ов преобразование данных на стороне CPU может оказаться довольно дорогой.

Для проверки того, что на данном устройстве для заданного формата изображения поддерживается описанная выше схема загрузки данных следует проверить наличие флага VK_FORMAT_FEATURE_2_HOST_IMAGE_TRANSFER_BIT_EXT. Но при этом гарантируется, что для всех форматов, поддерживающих VK_FORMAT_FEAUTURE_SAMPLED_IMAGE_BIT (т.е. поддерживающих чтение из шейдера), этот бит установлен. Ниже приводится пример кода, проверяющего эту поддержку.

bool    formatSupportHostCopy ( VkFormat format )
{
    VkFormatProperties2KHR formatProperties2 = {};
    VkFormatProperties3KHR formatProperties3 = {};

    formatProperties3.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3_KHR;
    formatProperties2.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2_KHR;
    formatProperties2.pNext = &formatProperties3;   // Properties3 need to be chained into Properties2

    vkGetPhysicalDeviceFormatProperties2 ( device.getPhysicalDevice (), format, &formatProperties2 );

    return (formatProperties3.optimalTilingFeatures & VK_FORMAT_FEATURE_2_HOST_IMAGE_TRANSFER_BIT_EXT) != 0;
}

Можно получить дополнительную информацию о быстродействии данной операции копирования через структуру VkHostImageCopyDevicePerformanceQuery. Для этого необходимо присоединить данную структуру к списку структур, передаваемых в вызов vkGetPhysicalDeviceImageFormatProperties2.

Обратите внимание, что всегда гарантируется, что все стандартные форматы с блочным сжатием имеют optimalDeviceAccess равный VK_TRUE.

typedef struct VkHostImageCopyDevicePerformanceQuery {
    VkStructureType    sType;
    void*              pNext;
    VkBool32           optimalDeviceAccess;
    VkBool32           identicalMemoryLayout;
} VkHostImageCopyDevicePerformanceQuery;
 
VkResult vkGetPhysicalDeviceImageFormatProperties2KHR (
    VkPhysicalDevice                            physicalDevice,
    const VkPhysicalDeviceImageFormatInfo2*     pImageFormatInfo,
    VkImageFormatProperties2*                   pImageFormatProperties );

Ниже приводится пример использующий данное расширение для загрузки текстуры.

#include    "VulkanWindow.h"
#include    "Buffer.h"
#include    "DescriptorSet.h"
#include    "stb_image.h"

struct Ubo
{
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

struct Vertex 
{
    glm::vec3 pos;
    glm::vec3 color;
    glm::vec2 texCoord;
};

template <>
GraphicsPipeline&   registerVertexAttrs<Vertex> ( GraphicsPipeline& pipeline ) 
{
    return pipeline
        .addVertexAttr     ( 0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, pos) )
        .addVertexAttr     ( 0, 1, VK_FORMAT_R32G32B32_SFLOAT, offsetof(Vertex, color) )
        .addVertexAttr     ( 0, 2, VK_FORMAT_R32G32_SFLOAT,    offsetof(Vertex, texCoord) );
}

const std::vector<Vertex> vertices = 
{
    {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f},  {1.0f, 0.0f}},
    {{0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f},  {0.0f, 0.0f}},
    {{0.5f, 0.5f,   0.0f}, {0.0f, 0.0f, 1.0f},  {0.0f, 1.0f}},
    {{-0.5f, 0.5f,  0.0f}, {1.0f, 1.0f, 1.0f},  {1.0f, 1.0f}},

    {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, -0.5f,  -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, 0.5f,   -0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
    {{-0.5f, 0.5f,  -0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};

const std::vector<uint16_t> indices = 
{
    0, 1, 2, 2, 3, 0,
    4, 5, 6, 6, 7, 4
};

class   ExampleWindow : public VulkanWindow
{
    std::vector<CommandBuffer>      commandBuffers;
    std::vector<DescriptorSet>      descriptorSets;
    std::vector<Uniform<Ubo>>       uniformBuffers;
    GraphicsPipeline                pipeline;
    Renderpass                      renderPass;
    Buffer                          vertexBuffer;
    Buffer                          indexBuffer;
    Texture                         texture;
    Sampler                         sampler;

    PFN_vkTransitionImageLayoutEXT              vkTransitionImageLayoutEXT              = {};
    PFN_vkCopyMemoryToImageEXT                  vkCopyMemoryToImageEXT                  = {};
    PFN_vkGetPhysicalDeviceFormatProperties2 vkGetPhysicalDeviceFormatProperties2 = {};

public:
    ExampleWindow ( int w, int h, const std::string& t, bool depth, DevicePolicy * p ) : VulkanWindow ( w, h, t, depth, p )
    {
        loadExtensions ();

        vertexBuffer.create  ( device, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, vertices, Buffer::hostWrite );
        indexBuffer.create   ( device, VK_BUFFER_USAGE_INDEX_BUFFER_BIT,  indices,  Buffer::hostWrite );
        sampler.setMinFilter ( VK_FILTER_LINEAR ).setMagFilter ( VK_FILTER_LINEAR ).create ( device );

        createTexture   ();
        createPipelines ();
    }

    void    loadExtensions ()
    {
        vkTransitionImageLayoutEXT           = reinterpret_cast<PFN_vkTransitionImageLayoutEXT>  (vkGetDeviceProcAddr(device.getDevice (), "vkTransitionImageLayoutEXT"));
        vkCopyMemoryToImageEXT               = reinterpret_cast<PFN_vkCopyMemoryToImageEXT>      (vkGetDeviceProcAddr(device.getDevice (), "vkCopyMemoryToImageEXT"));
        vkGetPhysicalDeviceFormatProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceFormatProperties2> (vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceFormatProperties2"));
    }

        // Check if the image format supports the host image copy flag
        // Note: All formats that support sampling are required to support this flag
    bool    formatSupportHostCopy ( VkFormat format )
    {
        VkFormatProperties2KHR formatProperties2 = {};
        VkFormatProperties3KHR formatProperties3 = {};

        formatProperties3.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3_KHR;
        formatProperties2.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2_KHR;
        formatProperties2.pNext = &formatProperties3;   // Properties3 need to be chained into Properties2

        vkGetPhysicalDeviceFormatProperties2 ( device.getPhysicalDevice (), format, &formatProperties2 );

        return (formatProperties3.optimalTilingFeatures & VK_FORMAT_FEATURE_2_HOST_IMAGE_TRANSFER_BIT_EXT) != 0;
    }

    void createTexture ()
    {
        int             texWidth, texHeight, texChannels;
        stbi_uc       * pixels    = stbi_load ( "textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha );
        VkDeviceSize    imageSize = texWidth * texHeight * 4;
        uint32_t        mipLevels = 1;  //static_cast<uint32_t>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;
        VkFormat        format    = VK_FORMAT_R8G8B8A8_UNORM;

        if ( !pixels )
            fatal () << "failed to load texture image!";

        if ( !formatSupportHostCopy ( format ) )
            fatal () << "Host image copy not supported for format" << std::endl;

            // TRANSFER_SRC for mipmap calculations via vkCmdBlitImage
        texture.create ( device, texWidth, texHeight, 1, mipLevels, format, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_HOST_TRANSFER_BIT, 0 /*VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT*/);

        std::vector<VkMemoryToImageCopyEXT> copies;
        VkImageSubresourceRange             subresourceRange = {};

        for ( uint32_t i = 0; i < mipLevels; i++ )
        {
                // Setup a buffer image copy structure for the current mip level
            VkMemoryToImageCopyEXT memoryCopy = {};

            memoryCopy.sType                           = VK_STRUCTURE_TYPE_MEMORY_TO_IMAGE_COPY_EXT;
            memoryCopy.imageSubresource.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
            memoryCopy.imageSubresource.mipLevel       = i;
            memoryCopy.imageSubresource.baseArrayLayer = 0;
            memoryCopy.imageSubresource.layerCount     = 1;
            memoryCopy.imageExtent.width               = texWidth  >> i;
            memoryCopy.imageExtent.height              = texHeight >> i;
            memoryCopy.imageExtent.depth               = 1;

                // This tells the implementation where to read the data from
            //ktx_size_t     offset;
            //KTX_error_code ret = ktxTexture_GetImageOffset(ktx_texture, i, 0, 0, &offset);
            //assert(ret == KTX_SUCCESS);
            memoryCopy.pHostPointer = pixels + 4 * i * memoryCopy.imageExtent.width * memoryCopy.imageExtent.height;

            copies.push_back ( memoryCopy );
        }

        subresourceRange.aspectMask   = VK_IMAGE_ASPECT_COLOR_BIT;
        subresourceRange.baseMipLevel = 0;
        subresourceRange.levelCount   = mipLevels;
        subresourceRange.layerCount   = 1;

        // VK_EXT_host_image_copy also introduces a simplified way of doing the required image transition on the host
        // This no longer requires a dedicated command buffer to submit the barrier
        // We also no longer need multiple transitions, and only have to do one for the final layout
        VkHostImageLayoutTransitionInfoEXT host_image_layout_transition_info = {};

        host_image_layout_transition_info.sType            = VK_STRUCTURE_TYPE_HOST_IMAGE_LAYOUT_TRANSITION_INFO_EXT;
        host_image_layout_transition_info.image            = texture.getImage().getHandle ();
        host_image_layout_transition_info.oldLayout        = VK_IMAGE_LAYOUT_UNDEFINED;
        host_image_layout_transition_info.newLayout        = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
        host_image_layout_transition_info.subresourceRange = subresourceRange;

        vkTransitionImageLayoutEXT ( device.getDevice (), 1, &host_image_layout_transition_info );

        // With the image in the correct layout and copy information for all mip levels setup, 
        // we can now issue the copy to our taget image from the host
        // The implementation will then convert this to an implementation specific optimal tiling layout
        VkCopyMemoryToImageInfoEXT copyInfo = {};

        copyInfo.sType          = VK_STRUCTURE_TYPE_COPY_MEMORY_TO_IMAGE_INFO_EXT;
        copyInfo.dstImage       = texture.getImage ().getHandle ();
        copyInfo.dstImageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
        copyInfo.regionCount    = uint32_t ( copies.size () );
        copyInfo.pRegions       = copies.data ();

        vkCopyMemoryToImageEXT ( device.getDevice (), &copyInfo );

        stbi_image_free ( pixels );
    }

    void    createUniformBuffers ()
    {
        uniformBuffers.resize ( swapChain.imageCount() );
        
        for ( size_t i = 0; i < swapChain.imageCount (); i++ )
            uniformBuffers [i].create ( device, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT );
    }

    void    freeUniformBuffers ()
    {
        uniformBuffers.clear ();
    }

    void    createDescriptorSets ()
    {
        descriptorSets.resize ( swapChain.imageCount () );

        for ( uint32_t i = 0; i < swapChain.imageCount (); i++ )
        {
            descriptorSets  [i]
                .setLayout        ( device, descAllocator, pipeline.getDescLayout () )
                .addUniformBuffer ( 0, uniformBuffers [i], 0, sizeof ( Ubo ) )
                .addImage         ( 1, texture, sampler )
                .create           ();
        }
    }
    
    virtual void    createPipelines () override 
    {
        createUniformBuffers    ();
        createDefaultRenderPass ( renderPass );

        pipeline.setDevice ( device )
                .setVertexShader   ( "shaders/shader.4.vert.spv" )
                .setFragmentShader ( "shaders/shader.4.frag.spv" )
                .setSize           ( swapChain.getExtent () )
                .addVertexBinding  ( sizeof ( Vertex ) )
                .addVertexAttributes <Vertex> ()
                .addDescLayout     ( 0, DescSetLayout ()
                    .add ( 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,         VK_SHADER_STAGE_VERTEX_BIT )
                    .add ( 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_FRAGMENT_BIT ) )
                .setCullMode       ( VK_CULL_MODE_NONE               )
                .create            ( renderPass );          

                // create before command buffers
        swapChain.createFramebuffers ( renderPass, depthTexture.getImageView () );

        createDescriptorSets ();
        createCommandBuffers ( renderPass );
    }

    virtual void    freePipelines () override
    {
        commandBuffers.clear ();
        pipeline.clean       ();
        renderPass.clean     ();
        freeUniformBuffers   ();
        descriptorSets.clear ();
        descAllocator.clean  ();
    }
    
    void    createCommandBuffers ( Renderpass& renderPass )
    {
        auto    framebuffers = swapChain.getFramebuffers ();

        commandBuffers = device.allocCommandBuffers ( (uint32_t)framebuffers.size ());

        for ( size_t i = 0; i < commandBuffers.size(); i++ )
        {
            commandBuffers [i].begin ().beginRenderPass ( RenderPassInfo ( renderPass ).framebuffer ( framebuffers [i] ).extent ( swapChain.getExtent () ).clearColor ().clearDepthStencil () )
                .pipeline          ( pipeline )
                .bindVertexBuffers ( { {vertexBuffer, 0} } )
                .bindIndexBuffer   ( indexBuffer )
                .addDescriptorSets ( { descriptorSets [i] } )
                .setViewport       ( swapChain.getExtent () )
                .setScissor        ( swapChain.getExtent () )
                .drawIndexed       ( static_cast<uint32_t>(indices.size ()) )
                .end ();
        }
    }

    virtual void    submit ( uint32_t imageIndex ) override 
    {
        updateUniformBuffer ( imageIndex );
        defaultSubmit       ( commandBuffers [imageIndex] );
    }

    void updateUniformBuffer ( uint32_t currentImage )
    {
        float   time = (float)getTime ();
        auto    fwd  = glm::vec3(0.0f, 0.0f, 1.0f);

        uniformBuffers [currentImage]->model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), fwd);
        uniformBuffers [currentImage]->view  = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), fwd);
        uniformBuffers [currentImage]->proj  = projectionMatrix ( 45, getAspect (), 0.1f, 10 );
    }
};

int main ( int argc, const char * argv [] ) 
{
    DevicePolicy    policy;

        // we require VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME device extension
    policy.addDeviceExtension ( VK_EXT_HOST_IMAGE_COPY_EXTENSION_NAME  );

    VkPhysicalDeviceHostImageCopyFeaturesEXT    hostCopyFeatures = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES_EXT };
    hostCopyFeatures.hostImageCopy = VK_TRUE;

    policy.addFeatures   ( &hostCopyFeatures );

    return ExampleWindow ( 800, 600, "Test window", true, &policy ).run ();
}