forked from Mrkol/graphics-course
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.cpp
More file actions
191 lines (160 loc) · 7.55 KB
/
App.cpp
File metadata and controls
191 lines (160 loc) · 7.55 KB
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#include "App.hpp"
#include <etna/Etna.hpp>
#include <etna/GlobalContext.hpp>
#include <etna/PipelineManager.hpp>
App::App()
: resolution{1280, 720}
, useVsync{true}
{
// First, we need to initialize Vulkan, which is not trivial because
// extensions are required for just about anything.
{
// GLFW tells us which extensions it needs to present frames to the OS window.
// Actually rendering anything to a screen is optional in Vulkan, you can
// alternatively save rendered frames into files, send them over network, etc.
// Instance extensions do not depend on the actual GPU, only on the OS.
auto glfwInstExts = windowing.getRequiredVulkanInstanceExtensions();
std::vector<const char*> instanceExtensions{glfwInstExts.begin(), glfwInstExts.end()};
// We also need the swapchain device extension to get access to the OS
// window from inside of Vulkan on the GPU.
// Device extensions require HW support from the GPU.
// Generally, in Vulkan, we call the GPU a "device" and the CPU/OS combination a "host."
std::vector<const char*> deviceExtensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
// Etna does all of the Vulkan initialization heavy lifting.
// You can skip figuring out how it works for now.
etna::initialize(etna::InitParams{
.applicationName = "Local Shadertoy",
.applicationVersion = VK_MAKE_VERSION(0, 1, 0),
.instanceExtensions = instanceExtensions,
.deviceExtensions = deviceExtensions,
// Replace with an index if etna detects your preferred GPU incorrectly
.physicalDeviceIndexOverride = {},
.numFramesInFlight = 1,
});
}
// Next, we need a magical Etna helper to send commands to the GPU.
// How it is actually performed is not trivial, but we can skip this for now.
commandManager = etna::get_context().createPerFrameCmdMgr();
// Now we can create an OS window
osWindow = windowing.createWindow(OsWindow::CreateInfo{
.resolution = resolution,
});
// But we also need to hook the OS window up to Vulkan manually!
{
// First, we ask GLFW to provide a "surface" for the window,
// which is an opaque description of the area where we can actually render.
auto surface = osWindow->createVkSurface(etna::get_context().getInstance());
// Then we pass it to Etna to do the complicated work for us
vkWindow = etna::get_context().createWindow(etna::Window::CreateInfo{
.surface = std::move(surface),
});
// And finally ask Etna to create the actual swapchain so that we can
// get (different) images each frame to render stuff into.
// Here, we do not support window resizing, so we only need to call this once.
auto [w, h] = vkWindow->recreateSwapchain(etna::Window::DesiredProperties{
.resolution = {resolution.x, resolution.y},
.vsync = useVsync,
.numFramesInFlight = static_cast<uint32_t>(commandManager->getCmdBufferCount()),
});
// Technically, Vulkan might fail to initialize a swapchain with the requested
// resolution and pick a different one. This, however, does not occur on platforms
// we support. Still, it's better to follow the "intended" path.
resolution = {w, h};
}
// TODO: Initialize any additional resources you require here!
}
App::~App()
{
ETNA_CHECK_VK_RESULT(etna::get_context().getDevice().waitIdle());
}
void App::run()
{
while (!osWindow->isBeingClosed())
{
windowing.poll();
drawFrame();
}
// We need to wait for the GPU to execute the last frame before destroying
// all resources and closing the application.
ETNA_CHECK_VK_RESULT(etna::get_context().getDevice().waitIdle());
}
void App::drawFrame()
{
// First, get a command buffer to write GPU commands into.
auto currentCmdBuf = commandManager->acquireNext();
// Next, tell Etna that we are going to start processing the next frame.
etna::begin_frame();
// And now get the image we should be rendering the picture into.
auto nextSwapchainImage = vkWindow->acquireNext();
// When window is minimized, we can't render anything in Windows
// because it kills the swapchain, so we skip frames in this case.
if (nextSwapchainImage)
{
auto [backbuffer, backbufferView, backbufferAvailableSem, backbufferReadyForPresentSem] =
*nextSwapchainImage;
ETNA_CHECK_VK_RESULT(currentCmdBuf.begin(vk::CommandBufferBeginInfo{}));
{
// First of all, we need to "initialize" th "backbuffer", aka the current swapchain
// image, into a state that is appropriate for us working with it. The initial state
// is considered to be "undefined" (aka "I contain trash memory"), by the way.
// "Transfer" in vulkanese means "copy or blit".
// Note that Etna sometimes calls this for you to make life simpler, read Etna's code!
etna::set_state(
currentCmdBuf,
backbuffer,
// We are going to use the texture at the transfer stage...
vk::PipelineStageFlagBits2::eTransfer,
// ...to transfer-write stuff into it...
vk::AccessFlagBits2::eTransferWrite,
// ...and want it to have the appropriate layout.
vk::ImageLayout::eTransferDstOptimal,
vk::ImageAspectFlagBits::eColor);
// The set_state doesn't actually record any commands, they are deferred to
// the moment you call flush_barriers.
// As with set_state, Etna sometimes flushes on it's own.
// Usually, flushes should be placed before "action", i.e. compute dispatches
// and blit/copy operations.
etna::flush_barriers(currentCmdBuf);
// TODO: Record your commands here!
// At the end of "rendering", we are required to change how the pixels of the
// swpchain image are laid out in memory to something that is appropriate
// for presenting to the window (while preserving the content of the pixels!).
etna::set_state(
currentCmdBuf,
backbuffer,
// This looks weird, but is correct. Ask about it later.
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
{},
vk::ImageLayout::ePresentSrcKHR,
vk::ImageAspectFlagBits::eColor);
// And of course flush the layout transition.
etna::flush_barriers(currentCmdBuf);
}
ETNA_CHECK_VK_RESULT(currentCmdBuf.end());
// We are done recording GPU commands now and we can send them to be executed by the GPU.
// Note that the GPU won't start executing our commands before the backbufferAvailableSem
// semaphore is signalled, which will happen when the OS says that the next swapchain image
// is ready, and the result image will be ready for present after backbufferReadyForPresent
// is signalled by GPU
auto renderingDone = commandManager->submit(
std::move(currentCmdBuf),
std::move(backbufferAvailableSem),
std::move(backbufferReadyForPresentSem));
// Finally, present the backbuffer the screen, but only after the GPU tells the OS
// that it is done executing the command buffer via the renderingDone semaphore.
const bool presented = vkWindow->present(std::move(renderingDone), backbufferView);
if (!presented)
nextSwapchainImage = std::nullopt;
}
etna::end_frame();
// After a window us un-minimized, we need to restore the swapchain to continue rendering.
if (!nextSwapchainImage && osWindow->getResolution() != glm::uvec2{0, 0})
{
auto [w, h] = vkWindow->recreateSwapchain(etna::Window::DesiredProperties{
.resolution = {resolution.x, resolution.y},
.vsync = useVsync,
.numFramesInFlight = static_cast<uint32_t>(commandManager->getCmdBufferCount()),
});
ETNA_VERIFY((resolution == glm::uvec2{w, h}));
}
}