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:
| // 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:
| // 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.
| .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
We create the texture and a view with the correct multisampling configuration and new dimensions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | void HelloTriangleMSAAWithDynamicRendering::createRenderTarget()
{
const TextureOptions options = {
.type = TextureType::TextureType2D,
.format = m_swapchainFormat,
.extent = { .width = m_swapchainExtent.width, .height = m_swapchainExtent.height, .depth = 1 },
.mipLevels = 1,
.samples = m_samples.get(),
.usage = TextureUsageFlagBits::ColorAttachmentBit,
.memoryUsage = MemoryUsage::GpuOnly,
.initialLayout = TextureLayout::Undefined
};
m_msaaTexture = m_device.createTexture(options);
m_msaaTextureView = m_msaaTexture.createView();
}
|
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 | const auto renderPassOptions = RenderPassCommandRecorderWithDynamicRenderingOptions{
.colorAttachments = {
{
// When using MSAA, we update the resolveView instead of the view
.view = isMsaaEnabled() ? m_msaaTextureView : m_swapchainViews.at(m_currentSwapchainImageIndex),
.resolveView = isMsaaEnabled() ? m_swapchainViews.at(m_currentSwapchainImageIndex) : Handle<TextureView_t>{},
.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.
| auto opaquePass = commandRecorder.beginRenderPass(renderPassOptions);
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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | 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,
},
});
commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
.srcStages = PipelineStageFlagBit::TopOfPipeBit,
.srcMask = AccessFlagBit::None,
.dstStages = PipelineStageFlagBit::EarlyFragmentTestBit,
.dstMask = AccessFlagBit::DepthStencilAttachmentReadBit | KDGpu::AccessFlagBit::DepthStencilAttachmentWriteBit,
.oldLayout = TextureLayout::Undefined,
.newLayout = TextureLayout::DepthStencilAttachmentOptimal,
.texture = m_depthTexture,
.range = TextureSubresourceRange{
.aspectMask = TextureAspectFlagBits::DepthBit | TextureAspectFlagBits::StencilBit,
},
});
if (isMsaaEnabled()) {
commandRecorder.textureMemoryBarrier(TextureMemoryBarrierOptions{
.srcStages = PipelineStageFlagBit::TopOfPipeBit,
.srcMask = AccessFlagBit::None,
.dstStages = PipelineStageFlagBit::ColorAttachmentOutputBit,
.dstMask = AccessFlagBit::ColorAttachmentWriteBit,
.oldLayout = TextureLayout::Undefined,
.newLayout = TextureLayout::ColorAttachmentOptimal,
.texture = m_msaaTexture,
.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-11-07 at 00:01:27 +0000