Mipmap generation : Transfers, transition layout

Hi guys !
This article will deal with Mipmap’s generation.

What is a Mipmap ?

A mipmap is a kind of loop rescaling on a texture.
For example, take one 512×128 texture, you will divide the size by 4 at each level :

  1. level 0 : 512×128
  2. level 1: 256×64
  3. level 2: 128×32
  4. level 3: 64×16
  5. level 4: 32×8
  6. level 5: 16×4
  7. level 6: 8×2
  8. level 7: 4×1
  9. level 8: 2×1
  10. level 9: 1×1

Mipmap

Why do we need Mipmap ?

It can be seen as a Level of Detail (LoD). If the object is far from the camera, you do not need to use all details but only some. So, instead to send to the GPU all the texture, you can send a mipmap with a different level that is far away lighter than the original (level 0).

How to generate Mipmap in Vulkan ?

Contrary to OpenGL which provide glGenerateTextureMipmap function, Vulkan does not provide function to build mipmap by itself. You have to deal with it by yourself. There are two ways.

  1. Using the shaders and framebuffers. You use the shader to draw into the framebuffer which is half the size of the texture, and half of the half…
  2. Using the transfer queue and vkCmdBlitImage which blit one image into another.

We are going to see the second way.
To do it, we are going to use the Transferer class we saw prior.

First, the number of mipmaps level for one image is :
levels=floor(log_2(max(width, height))) + 1

The idea of the algorithm to create the differents mipmap levels is easy.

  1. You initialize the level 0 (from a file for example) and put the layout to TRANSFER_SRC
  2. You set the level 1 to TRANSFER_DST
  3. You blit the level 0 to the level 1
  4. You set the level 1 to TRANSFER_SRC
  5. You reitere 2 3 4 for each level.
  6. You transition all levels to the layout you need.

So, here is our code :

Beginning with the CommandBuffer

void Transferer::buildMipMap(Image &src) {
    vk::CommandBuffer cmd = mCommandBufferSubmitter->createCommandBuffer(nullptr);
    vk::CommandBufferBeginInfo beginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);

    cmd.begin(beginInfo);

Prepare the blit !

    for(uint32_t i = 1; i < src.getMipLevels(); ++i) {
        vk::ImageBlit blit;
        blit.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
        blit.srcSubresource.baseArrayLayer = 0;
        blit.srcSubresource.layerCount = 1;
        blit.srcSubresource.mipLevel = i - 1;
        blit.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
        blit.dstSubresource.baseArrayLayer = 0;
        blit.dstSubresource.layerCount = 1;
        blit.dstSubresource.mipLevel = i;

        // each mipmap is the size divided by two
        blit.srcOffsets[1] = vk::Offset3D(std::max(1u, src.getSize().width >> (i - 1)),
                                          std::max(1u, src.getSize().height >> (i - 1)),
                                          1);

        blit.dstOffsets[1] = vk::Offset3D(std::max(1u, src.getSize().width >> i),
                                          std::max(1u, src.getSize().height >> i),
                                          1);

The loop begins from 1 because the level 0 is already initialized.
After, you explain to Vulkan which level you will use as the source, and which one you will use as the destination.
Do not forget to use max when you compute the offset because if you do not use it, you will be unable to build mipmap for the last levels if your image is not with a 1:1 ratio.

Transition and Blit

After, you have to transition your mipmap level image layout you want to draw into to TRANSFER_DST

 vk::ImageSubresourceRange range(vk::ImageAspectFlagBits::eColor, i, 1, 0, 1);
 // transferDst go to transferSrc because this mipmap will be the source for the next iteration (the next level)
 vk::ImageMemoryBarrier preBlit = transitionImage(src, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, range);
 cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
                     vk::PipelineStageFlagBits::eTransfer,
                     vk::DependencyFlags(),
                     nullptr, nullptr,
                     preBlit);

cmd.blitImage(src, vk::ImageLayout::eTransferSrcOptimal,
              src, vk::ImageLayout::eTransferDstOptimal, blit,
              vk::Filter::eLinear);

And you just use blitImage to blit it.

After, you have to transition the mipmap level image layout to TRANSFER_SRC

 vk::ImageMemoryBarrier postBlit = transitionImage(src, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal, range);
 cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                     vk::PipelineStageFlagBits::eTransfer,
                     vk::DependencyFlags(),
                     nullptr, nullptr,
                     postBlit);

Finish

You have to transition all mipmap levels to the layout you want to use

vk::ImageSubresourceRange range(vk::ImageAspectFlagBits::eColor, 0, VK_REMAINING_MIP_LEVELS, 0, 1);

// transition all mipmap levels to shaderReadOnlyOptimal
vk::ImageMemoryBarrier transition = transitionImage(src,
                                                    vk::ImageLayout::eTransferSrcOptimal,
                                                    vk::ImageLayout::eShaderReadOnlyOptimal,
                                                    range);

cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                    vk::PipelineStageFlagBits::eAllCommands,
                    vk::DependencyFlags(),
                    nullptr, nullptr, transition);

Conclusion

This article was short, but mipmap are not that difficult to handle. Do you like this kind of short article?
Maybe the next article will be about descriptor set management.

Reference

Sascha Willems Mipmap

Comments

9 responses to “Mipmap generation : Transfers, transition layout”

  1. Dmitry Avatar
    Dmitry

    Hello! Thanks a lot for the lessons. When you write tutorial about “Scene example”? You tell us in detail about renering steps (geometry, presentations and other). Just head explodes with new classes. And why should the division of the scene on the sites? You will write all this in tutorial about “Scene”?

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      I don’t know. There is a lot of possible solutions to “render” a scene on screen. So I do not think I will do such a tutorial. But maybe I’ll do some “algorithm” tutorial such as ambient occlusion, or forward + rendering vs deferred rendering, or global illumination. I cannot tell right now.
      But, you have the scene example here :
      https://github.com/qnope/Vulkan-Example/tree/master/Scene

  2. Dmitry Avatar
    Dmitry

    Hello! Please may you help me with adding depth testing in app. I do everything on your examples, but framebuffer does not want to be created.
    void cSwapchainKHR::CreateFrameBuffers()
    {
    m_FrameBuffer->clear();
    for (auto &imageView : *m_ImageView) {

    std::array attachments = {
    imageView,
    *m_DepthImageView
    };
    vk::FramebufferCreateInfo fb(vk::FramebufferCreateFlags(),
    *m_RenderPass, attachments.size(), attachments.data(),
    *m_Width, *m_Height, 1);

    m_FrameBuffer->emplace_back(*m_Device, fb); //Access violation at address 0x0000004C
    }
    }

    Before that I’m create DepthImage like that:
    vk::ImageCreateInfo depthInfo(vk::ImageCreateFlags(),
    vk::ImageType::e2D,
    vk::Format::eD24UnormS8Uint,
    vk::Extent3D(GetWidth(), GetHeight(), 1),
    1, 1,
    vk::SampleCountFlagBits::e1,
    vk::ImageTiling::eOptimal,
    vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled,
    vk::SharingMode::eExclusive,
    0,
    nullptr,
    vk::ImageLayout::eDepthStencilAttachmentOptimal);
    m_DepthImage = make_shared(*m_Device, depthInfo, Allocator); //eSUCCESS result

    In a next step I’m create ImageViews:
    m_ImageView->clear();

    for (vk::Image image : *m_Images) {
    vk::ImageViewCreateInfo info(vk::ImageViewCreateFlags(),
    image, vk::ImageViewType::e2D,
    m_Format->format, vk::ComponentMapping(),
    vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1));
    m_ImageView->emplace_back(*m_Device, info);

    }
    //Depth Image View
    vk::ImageViewCreateInfo DepthInfo(vk::ImageViewCreateFlags(),
    *m_DepthImage, vk::ImageViewType::e2D,
    vk::Format::eD24UnormS8Uint, vk::ComponentMapping(),
    vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil, 0, 1, 0, 1));

    m_DepthImageView = make_shared(*m_Device, DepthInfo); //eSUCCESS result

    But create buffers failed with two attachments:
    m_FrameBuffer->clear();
    for (auto &imageView : *m_ImageView) {

    std::array attachments = {
    imageView,
    *m_DepthImageView
    };
    //m_DepthImageView
    vk::FramebufferCreateInfo fb(vk::FramebufferCreateFlags(),
    *m_RenderPass, attachments.size(), attachments.data(),
    *m_Width, *m_Height, 1);

    m_FrameBuffer->emplace_back(*m_Device, fb); ////Access violation at address 0x0000004C

    Here code create of RenderPass (with two attachments):
    vk::AttachmentDescription colorAttachment(vk::AttachmentDescriptionFlags(),
    vk::Format::eB8G8R8A8Unorm, vk::SampleCountFlagBits::e1,
    vk::AttachmentLoadOp::eClear,
    vk::AttachmentStoreOp::eStore,
    vk::AttachmentLoadOp::eDontCare,
    vk::AttachmentStoreOp::eDontCare,
    vk::ImageLayout::eUndefined,
    vk::ImageLayout::ePresentSrcKHR);

    vk::AttachmentReference colorAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal);
    //depth
    vk::AttachmentDescription depthAttachment(vk::AttachmentDescriptionFlags(),

    vk::Format::eD24UnormS8Uint, vk::SampleCountFlagBits::e1,
    vk::AttachmentLoadOp::eClear,
    vk::AttachmentStoreOp::eDontCare,
    vk::AttachmentLoadOp::eDontCare,
    vk::AttachmentStoreOp::eDontCare,
    vk::ImageLayout::eDepthStencilAttachmentOptimal,
    vk::ImageLayout::eDepthStencilAttachmentOptimal);

    vk::AttachmentReference depthAttachmentRef(1, vk::ImageLayout::eDepthStencilAttachmentOptimal);

    // This subpass is a graphic one

    vk::SubpassDescription subPass(vk::SubpassDescriptionFlags(),
    vk::PipelineBindPoint::eGraphics, 0, nullptr,
    1, &colorAttachmentRef,nullptr,&depthAttachmentRef,0,nullptr);

    std::array attachments = { colorAttachment, depthAttachment };

    vk::RenderPassCreateInfo renderpass(vk::RenderPassCreateFlags(),
    attachments.size(), attachments.data(), 1, &subPass, 0, nullptr);

    m_renderPass = device.createRenderPass(renderpass); //eSUCCESS result

    Please help! What I’m doing wrong?? Why “m_FrameBuffer->emplace_back(*m_Device, fb);” Failed??

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      I do not understand what swapchain are for in your example. Swapchain must not have your depth image.
      What your layers tell you? What the debugger tells you?
      It is difficult to help you if you just tell me : it segfault here.
      Is your std::vector really allocated (since it is a pointer).
      Are the values you send to the Framebuffer constructor seem correct before it segfault?
      Does it segfault into Vulkan Library? Ou inside STL?
      Are your drivers up to date?

      Did you try my example? Does it work? Does it segfault as well?

  3. Dmitry Avatar
    Dmitry

    Thank you for the answers!
    Hello! How I may change resolution in Vilkan API? For example: window width: 1366, height: 768, I want resolution of render 640×480.
    I tryed change it in Swapchain creation (extent parametr in CreateInfo, ), in frameBuffer createInfo (also width and height). Played with a viewport parametr, but all in vain. Please say how i may this deal? Thank!

    1. Antoine MORRIER Avatar
      Antoine MORRIER

      Normally, you have to change the image size, frame buffer, surface (and check when it is out of date or not) and the frame buffer.
      It should be enough

      1. Dmitry Avatar
        Dmitry

        I’m change image size and frame buffer but not change surface (because it is window. Or i’m wrong and it’s not a window). I’m want that surface size (window size) will be is not changed but resolution image and frame buffer will be smaller. But validation layer telling me extent image and/or frame buffer not equal to extent surface.
        Therefore i’m not may make on window with width: 1366×768 resolution of render 640×480 for example??? Is there always a window resolution and rendering permission? I just wanted to make a choice of resolutions in some game settings.
        And second question about command buffers: i’m created PrimaryCommandPool, allocated from them PrimaryCommndBuffer. Then I created one SecondaryCommandPool on each thread (4 threads for example). How many secondary command buffers should I create for the best performance?
        1) One for rendering object (for example in one thread 1000 objects then 1000 secondary command buffers(it’s a lot? or not?) (nessessary write draw calls for each object in every secondary command buffer) ).
        2) Or one secondary command buffer on thread (1 for rendering 1000 objects (nessessary write all draw calls in 1 secondary command buffer) (it’s bad))
        How to do better in this case? Thanks for the advice!

        1. Antoine MORRIER Avatar
          Antoine MORRIER

          For the first question : If the layer tell you so, you need to recreate the surface. That’s why I talked about “ouf-of-date” or something like that. Look into the spec the return of vkAcquireNextImageKHR.

          For the second : profile.
          I’d go for one secondary buffer reusable for all static geometry, and create some batch. For exemple 4 threads = 4 secondary command buffer = 1 thread for people, 1 thread for cars, one thread for objects 1 thread for … (particle?).
          You need to profile it. Also, I am not a professional and I did not use Vulkan a lot, so i may be wrong as well ;).

Leave a Reply