Skip to content

Commit b0b12a6

Browse files
committed
Dynamic Rendering
1 parent 026c77a commit b0b12a6

File tree

6 files changed

+287
-1
lines changed

6 files changed

+287
-1
lines changed

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
- [Swapchain Loop](rendering/swapchain_loop.md)
2121
- [Render Sync](rendering/render_sync.md)
2222
- [Swapchain Update](rendering/swapchain_update.md)
23+
- [Dynamic Rendering](rendering/dynamic_rendering.md)
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Dynamic Rendering
2+
3+
Dynamic Rendering enables us to avoid using Render Passes, which are quite a bit more verbose (but also generally more performant on tiled GPUs). Here we tie together the Swapchain, Render Sync, and rendering.
4+
5+
In the main loop, attempt to acquire a Swapchain image / Render Target:
6+
7+
```cpp
8+
auto const framebuffer_size = glfw::framebuffer_size(m_window.get());
9+
// minimized? skip loop.
10+
if (framebuffer_size.x <= 0 || framebuffer_size.y <= 0) { continue; }
11+
// an eErrorOutOfDateKHR result is not guaranteed if the
12+
// framebuffer size does not match the Swapchain image size, check it
13+
// explicitly.
14+
auto fb_size_changed = framebuffer_size != m_swapchain->get_size();
15+
auto& render_sync = m_render_sync.at(m_frame_index);
16+
auto render_target = m_swapchain->acquire_next_image(*render_sync.draw);
17+
if (fb_size_changed || !render_target) {
18+
m_swapchain->recreate(framebuffer_size);
19+
continue;
20+
}
21+
```
22+
23+
Wait for the associated fence and reset ('un'signal) it:
24+
25+
```cpp
26+
static constexpr auto fence_timeout_v =
27+
static_cast<std::uint64_t>(std::chrono::nanoseconds{3s}.count());
28+
auto result = m_device->waitForFences(*render_sync.drawn, vk::True,
29+
fence_timeout_v);
30+
if (result != vk::Result::eSuccess) {
31+
throw std::runtime_error{"Failed to wait for Render Fence"};
32+
}
33+
// reset fence _after_ acquisition of image: if it fails, the
34+
// fence remains signaled.
35+
m_device->resetFences(*render_sync.drawn);
36+
```
37+
38+
Since the fence has been reset, a queue submission must be made that signals it before continuing, otherwise the app will deadlock on the next wait (and eventually throw after 3s). We can now begin command buffer recording:
39+
40+
```cpp
41+
auto command_buffer_bi = vk::CommandBufferBeginInfo{};
42+
// this flag means recorded commands will not be reused.
43+
command_buffer_bi.setFlags(
44+
vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
45+
render_sync.command_buffer.begin(command_buffer_bi);
46+
```
47+
48+
We are not ready to actually render anything yet, but can clear the image to a particular color. First we need to transition the image for rendering, ie Attachment Optimal layout. Set up the image barrier and record it:
49+
50+
```cpp
51+
auto dependency_info = vk::DependencyInfo{};
52+
auto barrier = m_swapchain->base_barrier();
53+
// Undefined => AttachmentOptimal
54+
// we don't need to block any operations before the barrier, since we
55+
// rely on the image acquired semaphore to block rendering.
56+
// any color attachment operations must happen after the barrier.
57+
barrier.setOldLayout(vk::ImageLayout::eUndefined)
58+
.setNewLayout(vk::ImageLayout::eAttachmentOptimal)
59+
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
60+
.setSrcStageMask(vk::PipelineStageFlagBits2::eTopOfPipe)
61+
.setDstAccessMask(vk::AccessFlagBits2::eColorAttachmentWrite)
62+
.setDstStageMask(
63+
vk::PipelineStageFlagBits2::eColorAttachmentOutput);
64+
dependency_info.setImageMemoryBarriers(barrier);
65+
render_sync.command_buffer.pipelineBarrier2(dependency_info);
66+
```
67+
68+
Create an Rendering Attachment Info using the acquired image as the color target. We use a red clear color, make sure the Load Op clears the image, and Store Op stores the results (currently just the cleared image):
69+
70+
```cpp
71+
auto attachment_info = vk::RenderingAttachmentInfo{};
72+
attachment_info.setImageView(render_target->image_view)
73+
.setImageLayout(vk::ImageLayout::eAttachmentOptimal)
74+
.setLoadOp(vk::AttachmentLoadOp::eClear)
75+
.setStoreOp(vk::AttachmentStoreOp::eStore)
76+
.setClearValue(vk::ClearColorValue{1.0f, 0.0f, 0.0f, 1.0f});
77+
```
78+
79+
Set up a Rendering Info object with the color attachment and the entire image as the render area:
80+
81+
```cpp
82+
auto rendering_info = vk::RenderingInfo{};
83+
auto const render_area =
84+
vk::Rect2D{vk::Offset2D{}, render_target->extent};
85+
rendering_info.setRenderArea(render_area)
86+
.setColorAttachments(attachment_info)
87+
.setLayerCount(1);
88+
```
89+
90+
Finally, execute a render:
91+
92+
```cpp
93+
render_sync.command_buffer.beginRendering(rendering_info);
94+
// draw stuff here.
95+
render_sync.command_buffer.endRendering();
96+
```
97+
98+
Transition the image for presentation:
99+
100+
```cpp
101+
// AttachmentOptimal => PresentSrc
102+
// the barrier must wait for color attachment operations to complete.
103+
// we don't need any post-synchronization as the present Sempahore takes
104+
// care of that.
105+
barrier.setOldLayout(vk::ImageLayout::eAttachmentOptimal)
106+
.setNewLayout(vk::ImageLayout::ePresentSrcKHR)
107+
.setSrcAccessMask(vk::AccessFlagBits2::eColorAttachmentWrite)
108+
.setSrcStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput)
109+
.setDstAccessMask(vk::AccessFlagBits2::eNone)
110+
.setDstStageMask(vk::PipelineStageFlagBits2::eBottomOfPipe);
111+
dependency_info.setImageMemoryBarriers(barrier);
112+
render_sync.command_buffer.pipelineBarrier2(dependency_info);
113+
```
114+
115+
End the command buffer and submit it:
116+
117+
```cpp
118+
render_sync.command_buffer.end();
119+
120+
auto submit_info = vk::SubmitInfo2{};
121+
auto const command_buffer_info =
122+
vk::CommandBufferSubmitInfo{render_sync.command_buffer};
123+
auto wait_semaphore_info = vk::SemaphoreSubmitInfo{};
124+
wait_semaphore_info.setSemaphore(*render_sync.draw)
125+
.setStageMask(vk::PipelineStageFlagBits2::eTopOfPipe);
126+
auto signal_semaphore_info = vk::SemaphoreSubmitInfo{};
127+
signal_semaphore_info.setSemaphore(*render_sync.present)
128+
.setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput);
129+
submit_info.setCommandBufferInfos(command_buffer_info)
130+
.setWaitSemaphoreInfos(wait_semaphore_info)
131+
.setSignalSemaphoreInfos(signal_semaphore_info);
132+
m_queue.submit2(submit_info, *render_sync.drawn);
133+
```
134+
135+
The `draw` Semaphore will be signaled by the Swapchain when the image is ready, which will trigger this command buffer's execution. It will signal the `present` Semaphore and `drawn` Fence on completion, with the latter being waited on the next time this virtual frame is processed. Finally, we increment the frame index, pass the `present` semaphore as the one for the subsequent present operation to wait on:
136+
137+
```cpp
138+
m_frame_index = (m_frame_index + 1) % m_render_sync.size();
139+
140+
if (!m_swapchain->present(m_queue, *render_sync.present)) {
141+
m_swapchain->recreate(framebuffer_size);
142+
continue;
143+
}
144+
```
145+
146+
> Wayland users: congratulaions, you can finally see and interact with the window!
147+
148+
![Cleared Image](./dynamic_rendering_red_clear.png)
149+
150+
## Render Doc on Wayland
151+
152+
At the time of writing, RenderDoc doesn't support inspecting Wayland applications. Temporarily force X11 (XWayland) by calling `glfwInitHint()` before `glfwInit()`:
153+
154+
```cpp
155+
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
156+
```
157+
158+
Setting up a command line option to conditionally call this is a simple and flexible approach: just set that argument in RenderDoc itself and/or pass it whenever an X11 backend is desired:
159+
160+
```cpp
161+
// main.cpp
162+
// skip the first argument.
163+
auto args = std::span{argv, static_cast<std::size_t>(argc)}.subspan(1);
164+
while (!args.empty()) {
165+
auto const arg = std::string_view{args.front()};
166+
if (arg == "-x" || arg == "--force-x11") {
167+
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
168+
}
169+
args = args.subspan(1);
170+
}
171+
lvk::App{}.run();
172+
```
18.2 KB
Loading
1.27 MB
Loading

src/app.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#include <app.hpp>
22
#include <cassert>
3+
#include <chrono>
34
#include <print>
45
#include <ranges>
56

67
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
78

89
namespace lvk {
10+
using namespace std::chrono_literals;
11+
912
void App::run() {
1013
create_window();
1114
create_instance();
@@ -127,6 +130,106 @@ void App::create_render_sync() {
127130
void App::main_loop() {
128131
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
129132
glfwPollEvents();
133+
134+
auto const framebuffer_size = glfw::framebuffer_size(m_window.get());
135+
// minimized? skip loop.
136+
if (framebuffer_size.x <= 0 || framebuffer_size.y <= 0) { continue; }
137+
// an eErrorOutOfDateKHR result is not guaranteed if the
138+
// framebuffer size does not match the Swapchain image size, check it
139+
// explicitly.
140+
auto fb_size_changed = framebuffer_size != m_swapchain->get_size();
141+
auto& render_sync = m_render_sync.at(m_frame_index);
142+
auto render_target = m_swapchain->acquire_next_image(*render_sync.draw);
143+
if (fb_size_changed || !render_target) {
144+
m_swapchain->recreate(framebuffer_size);
145+
continue;
146+
}
147+
148+
static constexpr auto fence_timeout_v =
149+
static_cast<std::uint64_t>(std::chrono::nanoseconds{3s}.count());
150+
auto result = m_device->waitForFences(*render_sync.drawn, vk::True,
151+
fence_timeout_v);
152+
if (result != vk::Result::eSuccess) {
153+
throw std::runtime_error{"Failed to wait for Render Fence"};
154+
}
155+
// reset fence _after_ acquisition of image: if it fails, the
156+
// fence remains signaled.
157+
m_device->resetFences(*render_sync.drawn);
158+
159+
auto command_buffer_bi = vk::CommandBufferBeginInfo{};
160+
// this flag means recorded commands will not be reused.
161+
command_buffer_bi.setFlags(
162+
vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
163+
render_sync.command_buffer.begin(command_buffer_bi);
164+
165+
auto dependency_info = vk::DependencyInfo{};
166+
auto barrier = m_swapchain->base_barrier();
167+
// Undefined => AttachmentOptimal
168+
// we don't need to block any operations before the barrier, since we
169+
// rely on the image acquired semaphore to block rendering.
170+
// any color attachment operations must happen after the barrier.
171+
barrier.setOldLayout(vk::ImageLayout::eUndefined)
172+
.setNewLayout(vk::ImageLayout::eAttachmentOptimal)
173+
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
174+
.setSrcStageMask(vk::PipelineStageFlagBits2::eTopOfPipe)
175+
.setDstAccessMask(vk::AccessFlagBits2::eColorAttachmentWrite)
176+
.setDstStageMask(
177+
vk::PipelineStageFlagBits2::eColorAttachmentOutput);
178+
dependency_info.setImageMemoryBarriers(barrier);
179+
render_sync.command_buffer.pipelineBarrier2(dependency_info);
180+
181+
auto attachment_info = vk::RenderingAttachmentInfo{};
182+
attachment_info.setImageView(render_target->image_view)
183+
.setImageLayout(vk::ImageLayout::eAttachmentOptimal)
184+
.setLoadOp(vk::AttachmentLoadOp::eClear)
185+
.setStoreOp(vk::AttachmentStoreOp::eStore)
186+
.setClearValue(vk::ClearColorValue{1.0f, 0.0f, 0.0f, 1.0f});
187+
auto rendering_info = vk::RenderingInfo{};
188+
auto const render_area =
189+
vk::Rect2D{vk::Offset2D{}, render_target->extent};
190+
rendering_info.setRenderArea(render_area)
191+
.setColorAttachments(attachment_info)
192+
.setLayerCount(1);
193+
194+
render_sync.command_buffer.beginRendering(rendering_info);
195+
// draw stuff here.
196+
render_sync.command_buffer.endRendering();
197+
198+
// AttachmentOptimal => PresentSrc
199+
// the barrier must wait for color attachment operations to complete.
200+
// we don't need any post-synchronization as the present Sempahore takes
201+
// care of that.
202+
barrier.setOldLayout(vk::ImageLayout::eAttachmentOptimal)
203+
.setNewLayout(vk::ImageLayout::ePresentSrcKHR)
204+
.setSrcAccessMask(vk::AccessFlagBits2::eColorAttachmentWrite)
205+
.setSrcStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput)
206+
.setDstAccessMask(vk::AccessFlagBits2::eNone)
207+
.setDstStageMask(vk::PipelineStageFlagBits2::eBottomOfPipe);
208+
dependency_info.setImageMemoryBarriers(barrier);
209+
render_sync.command_buffer.pipelineBarrier2(dependency_info);
210+
211+
render_sync.command_buffer.end();
212+
213+
auto submit_info = vk::SubmitInfo2{};
214+
auto const command_buffer_info =
215+
vk::CommandBufferSubmitInfo{render_sync.command_buffer};
216+
auto wait_semaphore_info = vk::SemaphoreSubmitInfo{};
217+
wait_semaphore_info.setSemaphore(*render_sync.draw)
218+
.setStageMask(vk::PipelineStageFlagBits2::eTopOfPipe);
219+
auto signal_semaphore_info = vk::SemaphoreSubmitInfo{};
220+
signal_semaphore_info.setSemaphore(*render_sync.present)
221+
.setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput);
222+
submit_info.setCommandBufferInfos(command_buffer_info)
223+
.setWaitSemaphoreInfos(wait_semaphore_info)
224+
.setSignalSemaphoreInfos(signal_semaphore_info);
225+
m_queue.submit2(submit_info, *render_sync.drawn);
226+
227+
m_frame_index = (m_frame_index + 1) % m_render_sync.size();
228+
229+
if (!m_swapchain->present(m_queue, *render_sync.present)) {
230+
m_swapchain->recreate(framebuffer_size);
231+
continue;
232+
}
130233
}
131234
}
132235
} // namespace lvk

src/main.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
#include <app.hpp>
22
#include <exception>
33
#include <print>
4+
#include <span>
45

5-
auto main() -> int {
6+
auto main(int argc, char** argv) -> int {
67
try {
8+
// skip the first argument.
9+
auto args = std::span{argv, static_cast<std::size_t>(argc)}.subspan(1);
10+
while (!args.empty()) {
11+
auto const arg = std::string_view{args.front()};
12+
if (arg == "-x" || arg == "--force-x11") {
13+
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
14+
}
15+
args = args.subspan(1);
16+
}
717
lvk::App{}.run();
818
} catch (std::exception const& e) {
919
std::println(stderr, "PANIC: {}", e.what());

0 commit comments

Comments
 (0)