Search code examples
texturesvulkan

Effective way to avoid changing texture layout in Vulkan every frame


I have some texture data that updates very rarely but I cannot control when it updates (data is provided from an external library that renders GUI).

Currently, I change their layout twice in every frame:

  1. from VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL with pipeline stages VK_PIPELINE_STAGE_TRANSFER_BIT to VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
  2. and then from VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL with stages src=VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT to dst=VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT.

I think, such transformations that happen every time may be wasteful because I almost always don't have any updates for my textures, so I am thinking about implementing this procedure:

  1. Check the updates.
  2. If there are any updates, record and submit a separate command buffer that transforms my textures from read-only optimal to transfer dst optimal.
  3. If there are any updates, record transfer commands and issue transform back to read-only optimal in main command buffer.
  4. Otherwise, just use textures as is without any transformations.

Is this a good approach? Are there any other ways to accomplish a reduction of such layout transforms? I also consider just using VK_IMAGE_LAYOUT_GENERAL.


Solution

  • I just understood that I forgot that I can track the state of texture myself.

    For example, in my question, there is 3 possible states and corresponding layouts of a texture:

    1. It is just created: VK_IMAGE_LAYOUT_UNDEFINED
    2. It was used in graphics pipeline and didn't have any transfers into it recently: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
    3. There was a transfer into it so it has layout VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL.

    So we can store a state in a enum:

    enum TextureState{
       just_created,
       used_for_render,
       had_transfer,
    }
    

    And access it during recording our command buffers every frame (pseudocode):

    // Copying data to texture.
    if (need_to_copy_to_texture) {
       if (tex_state == TextureState::had_transfer) {
          // Do nothing, we already in compatible layout
       }
       else if (tex_state == TextureState::just_created) {
          // Need VkImageMemoryBarrier here
          pipeline barrier VK_IMAGE_LAYOUT_UNDEFINED =>
               VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
       }
       else if (tex_state == TextureState::used_for_render) {
          // Need VkImageMemoryBarrier here
          pipeline barrier VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL =>
               VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
       }
       record copy of data to texture;
       tex_state = TextureState::had_transfer;
    }
    
    // Now we want to render
    if (tex_state == TextureState::used_for_render) {
      // Do nothing, we already in compatible layout for rendering
      // because there weren't any transfers after last rendering.
    }
    else if (tex_state == TextureState::just_created) {
      // This probably should not happen.
      assert(false);
    }
    else if (tex_state == TextureState::had_transfer) {
      // Need VkImageMemoryBarrier here
      pipeline barrier VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL =>
           VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
    }
    tex_state = TextureState::used_for_render;
    
    begin rendering;
    bind pipeline with our texture;
    bind descriptor set with out texture;
    issue draw call;
    end rendering;
    
    submit command buffer to a queue;
    

    This way we ensure that:

    1. We always have correct layout of texture for every operation.
    2. We do transfers between texture layouts only when it is required so no wasting GPU cycles doing unneeded job.