Skip to content

Hello Triangle MSAA with Dynamic Rendering

This example shows modern Vulkan dynamic rendering which eliminates the need for VkRenderPass and VkFramebuffer objects. Dynamic rendering (Vulkan 1.3 core) simplifies rendering by specifying attachments directly in command buffers rather than pre-creating render pass objects. This reduces boilerplate, improves flexibility, and better matches modern GPU architectures. Compare with Hello Triangle MSAA to see the differences.

The example uses the KDGpuExample helper API for simplified setup.

Overview

What this example demonstrates:

  • Enabling VK_KHR_dynamic_rendering extension
  • Creating pipelines compatible with dynamic rendering
  • Specifying attachments per command buffer (not pre-created)
  • Manual layout transitions (no implicit conversions)
  • MSAA with dynamic rendering

Use cases:

  • Simplified rendering setup
  • Dynamic attachment configurations
  • Modern Vulkan 1.3+ applications
  • Reducing object creation overhead

Vulkan Requirements

  • Vulkan Version: 1.3+ (dynamic rendering core) or 1.2+ with extension
  • Extensions: VK_KHR_dynamic_rendering (if Vulkan < 1.3)
  • Features: dynamicRendering

Key Concepts

Traditional Render Passes:

1
2
3
4
5
6
7
8
// Pre-create render pass object:
VkRenderPass renderPass = createRenderPass(attachmentFormats, ...);
VkFramebuffer framebuffer = createFramebuffer(renderPass, attachments);

// Rendering:
vkCmdBeginRenderPass(renderPass, framebuffer, ...);
// draw calls
vkCmdEndRenderPass();

Problems:

  • Render passes are complex to create
  • Framebuffers tie attachments to render passes
  • Poor flexibility for dynamic scenarios

Dynamic Rendering:

1
2
3
4
5
6
7
8
// No pre-creation! Specify attachments per command buffer:
vkCmdBeginRendering({
    .colorAttachments = {colorView, MSAA_RESOLVE, ...},
    .depthAttachment = depthView,
    ...
});
// draw calls
vkCmdEndRendering();

Benefits:

  • No VkRenderPass/VkFramebuffer objects
  • Attachments specified when needed
  • Better for dynamic/procedural rendering
  • Less validation overhead

Spec: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_dynamic_rendering.html

The first place to look is mkPipelineOptions, where the dynamicRendering flag is set. This function is called when the GraphicsPipeline are created and tells our pipeline will be using dynamic rendering.

1
2
3
4
5
6
            .multisample = {
                    .samples = samples,
            },
            .dynamicRendering = {
                    .enabled = true, // Mark that we want to use it with dynamic rendering
            },

Filename: hello_triangle_msaa_dynamic_rendering/hello_triangle_msaa_dynamic_rendering.cpp

Next, we create the texture and a view with the correct multisampling configuration and new dimensions, which we will attach to the render pass option struct. Note that we use the type KDGpu::RenderPassCommandRecorderWithDynamicRenderingOptions instead of KDGpu::RenderPassCommandRecorderOptions. Whilst using RenderPassCommandRecorderOptions would lead to an internal implicit RenderPass being created to hold the attachment specifications, using KDGpu::RenderPassCommandRecorderWithDynamicRenderingOptions will use Vulkan's dynamic rendering extension to more efficiently and conveniently specify these information. Internally this means we don't have to maintain RenderPasses, Framebuffers ...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    m_commandRecorderOptions = RenderPassCommandRecorderWithDynamicRenderingOptions {
        .colorAttachments = {
            {
                .view = m_msaaTextureView, // The multisampled view which will change on resize.
                .resolveView = {}, // Not setting the swapchain texture view just yet. That's handled at render.
                .clearValue = { 0.3f, 0.3f, 0.3f, 1.0f },
                .finalLayout = TextureLayout::PresentSrc
            }
        },
        .depthStencilAttachment = {
            .view = m_depthTextureView,
        },
        // configure for multisampling
        .samples = m_samples.get()
    };

Filename: hello_triangle_msaa_dynamic_rendering/hello_triangle_msaa_dynamic_rendering.cpp

The rendering submission itself doesn't change much. The noticeable change is we call renderImGuiOverlayDynamic instead of renderImGuiOverlay to render the ImGui overlay. That is so that we use a version that has a pipeline compatible with dynamic rendering.

1
2
3
4
5
6
7
8
9
    auto opaquePass = commandRecorder.beginRenderPass(m_commandRecorderOptions);
    opaquePass.setPipeline(m_pipelines[m_currentPipelineIndex]);
    opaquePass.setVertexBuffer(0, m_buffer);
    opaquePass.setIndexBuffer(m_indexBuffer);
    opaquePass.setBindGroup(0, m_transformBindGroup);
    const DrawIndexedCommand drawCmd = { .indexCount = 3 };
    opaquePass.drawIndexed(drawCmd);
    renderImGuiOverlayDynamic(&opaquePass);
    opaquePass.end();

Filename: hello_triangle_msaa_dynamic_rendering/hello_triangle_msaa_dynamic_rendering.cpp

Notice that with dynamic rendering, there is no implicit initial and final layout conversions of the attachments. This means we have to take care of doing this part ourselves.

Prior to rendering:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
            .srcStages = PipelineStageFlagBit::TopOfPipeBit,
            .srcMask = AccessFlagBit::None,
            .dstStages = PipelineStageFlagBit::ColorAttachmentOutputBit,
            .dstMask = AccessFlagBit::ColorAttachmentWriteBit,
            .oldLayout = TextureLayout::Undefined,
            .newLayout = TextureLayout::ColorAttachmentOptimal,
            .texture = m_swapchain.textures().at(m_currentSwapchainImageIndex),
            .range = TextureSubresourceRange{
                    .aspectMask = TextureAspectFlagBits::ColorBit,
            },
    });

Filename: hello_triangle_msaa_dynamic_rendering/hello_triangle_msaa_dynamic_rendering.cpp

and after rendering, prior to presenting:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
            .srcStages = PipelineStageFlagBit::AllGraphicsBit,
            .srcMask = AccessFlagBit::ColorAttachmentWriteBit,
            .dstStages = PipelineStageFlagBit::BottomOfPipeBit,
            .dstMask = AccessFlagBit::MemoryReadBit,
            .oldLayout = TextureLayout::ColorAttachmentOptimal,
            .newLayout = TextureLayout::PresentSrc,
            .texture = m_swapchain.textures().at(m_currentSwapchainImageIndex),
            .range = TextureSubresourceRange{
                    .aspectMask = TextureAspectFlagBits::ColorBit,
            },
    });

Filename: hello_triangle_msaa_dynamic_rendering/hello_triangle_msaa_dynamic_rendering.cpp


Updated on 2026-03-31 at 00:02:07 +0000