TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
283 / 338
83.7%
Branches:
130 / 212
61.3%
Functions:
32 / 36
88.9%

gpu/vulkan.cpp
Line Branch Exec Source
1 #include "terraingen/gpu/vulkan.h"
2
3 #include "terraingen/util/log.h"
4
5 #include <volk.h>
6
7 #include <algorithm>
8 #include <cstdlib>
9 #include <cstring>
10 #include <map>
11 #include <mutex>
12 #include <stdexcept>
13 #include <tuple>
14 #include <utility>
15
16 namespace terraingen {
17
18 namespace {
19
20 /// Throws std::runtime_error naming `what` and the result unless `result` is VK_SUCCESS.
21 1291 void Check(VkResult result, const char* what) {
22
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1291 times.
1291 if (result != VK_SUCCESS) {
23
0/4
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 10 not taken.
✗ throw std::runtime_error(std::string("Vulkan ") + what + " failed (" +
24
0/2
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ std::to_string(static_cast<int>(result)) + ")");
25 }
26 1291 }
27
28 /// A pipeline for one kernel and buffer count.
29 struct Pipeline {
30 VkShaderModule module = VK_NULL_HANDLE;
31 VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
32 VkPipelineLayout layout = VK_NULL_HANDLE;
33 VkPipeline pipeline = VK_NULL_HANDLE;
34 };
35
36 /// Destroys what `p` holds; Vulkan ignores null handles, so a partly made pipeline is fine.
37 ✗ void DestroyPipeline(VkDevice device, const Pipeline& p) {
38 ✗ vkDestroyPipeline(device, p.pipeline, nullptr);
39 ✗ vkDestroyPipelineLayout(device, p.layout, nullptr);
40 ✗ vkDestroyDescriptorSetLayout(device, p.setLayout, nullptr);
41 ✗ vkDestroyShaderModule(device, p.module, nullptr);
42 }
43
44 20 VkShaderModule CreateShaderModule(VkDevice device, const ComputeKernel& kernel) {
45 20 VkShaderModuleCreateInfo info{};
46 20 info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
47 20 info.codeSize = kernel.size;
48 20 info.pCode = reinterpret_cast<const std::uint32_t*>(kernel.spirv);
49 20 VkShaderModule module = VK_NULL_HANDLE;
50
2/2
✓ Branch 1 taken 20 times.
✓ Branch 4 taken 20 times.
20 Check(vkCreateShaderModule(device, &info, nullptr, &module), "shader module");
51 20 return module;
52 }
53
54 /// A layout of `bindings` storage buffers, bound in order.
55 20 VkDescriptorSetLayout CreateSetLayout(VkDevice device, std::size_t bindings) {
56
1/1
✓ Branch 1 taken 20 times.
20 std::vector<VkDescriptorSetLayoutBinding> layoutBindings(bindings);
57
2/2
✓ Branch 0 taken 111 times.
✓ Branch 1 taken 20 times.
131 for (std::size_t i = 0; i < bindings; ++i) {
58 111 layoutBindings[i] = {static_cast<std::uint32_t>(i), VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1,
59 VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
60 }
61 20 VkDescriptorSetLayoutCreateInfo info{};
62 20 info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
63 20 info.bindingCount = static_cast<std::uint32_t>(bindings);
64 20 info.pBindings = layoutBindings.data();
65 20 VkDescriptorSetLayout layout = VK_NULL_HANDLE;
66
2/2
✓ Branch 1 taken 20 times.
✓ Branch 4 taken 20 times.
20 Check(vkCreateDescriptorSetLayout(device, &info, nullptr, &layout), "descriptor set layout");
67 20 return layout;
68 20 }
69
70 20 VkPipelineLayout CreatePipelineLayout(VkDevice device, VkDescriptorSetLayout setLayout,
71 std::size_t pushSize) {
72 20 const VkPushConstantRange range{VK_SHADER_STAGE_COMPUTE_BIT, 0,
73 20 static_cast<std::uint32_t>(std::max<std::size_t>(pushSize, 4))};
74 20 VkPipelineLayoutCreateInfo info{};
75 20 info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
76 20 info.setLayoutCount = 1;
77 20 info.pSetLayouts = &setLayout;
78
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 info.pushConstantRangeCount = pushSize > 0 ? 1u : 0u;
79 20 info.pPushConstantRanges = &range;
80 20 VkPipelineLayout layout = VK_NULL_HANDLE;
81
2/2
✓ Branch 1 taken 20 times.
✓ Branch 4 taken 20 times.
20 Check(vkCreatePipelineLayout(device, &info, nullptr, &layout), "pipeline layout");
82 20 return layout;
83 }
84
85 /// The pipeline running `kernel` with `bindings` storage buffers and `pushSize` bytes of push
86 /// constants; nothing is left behind when a step fails.
87 20 Pipeline CreatePipeline(VkDevice device, const ComputeKernel& kernel, std::size_t bindings,
88 std::size_t pushSize) {
89 20 Pipeline p;
90 try {
91
1/1
✓ Branch 1 taken 20 times.
20 p.module = CreateShaderModule(device, kernel);
92
1/1
✓ Branch 1 taken 20 times.
20 p.setLayout = CreateSetLayout(device, bindings);
93
1/1
✓ Branch 1 taken 20 times.
20 p.layout = CreatePipelineLayout(device, p.setLayout, pushSize);
94 20 VkComputePipelineCreateInfo info{};
95 20 info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
96 20 info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
97 20 info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
98 20 info.stage.module = p.module;
99 20 info.stage.pName = "main";
100 20 info.layout = p.layout;
101
2/2
✓ Branch 1 taken 20 times.
✓ Branch 4 taken 20 times.
20 Check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &p.pipeline),
102 "compute pipeline");
103 ✗ } catch (...) {
104
0/1
✗ Branch 1 not taken.
✗ DestroyPipeline(device, p);
105 ✗ throw;
106 }
107 20 return p;
108 }
109
110 /// A host-visible storage buffer.
111 struct Buffer {
112 VkBuffer buffer = VK_NULL_HANDLE;
113 VkDeviceMemory memory = VK_NULL_HANDLE;
114 void* mapped = nullptr;
115 };
116
117 /// The first memory type allowed by `bits` that has all of `flags`.
118 213 std::uint32_t MemoryType(const VkPhysicalDeviceMemoryProperties& memory, std::uint32_t bits,
119 VkMemoryPropertyFlags flags) {
120
1/2
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
213 for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) {
121
2/4
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 213 times.
✗ Branch 3 not taken.
213 if ((bits & (1u << i)) != 0 && (memory.memoryTypes[i].propertyFlags & flags) == flags) {
122 213 return i;
123 }
124 }
125
0/1
✗ Branch 2 not taken.
✗ throw std::runtime_error("No host-visible, coherent Vulkan memory type");
126 }
127
128 /// The memory type for buffers the host writes and reads back: host-cached where the device has
129 /// one, since reading results back from uncached memory is many times slower, otherwise any
130 /// host-visible, coherent type.
131 213 std::uint32_t HostMemoryType(const VkPhysicalDeviceMemoryProperties& memory, std::uint32_t bits) {
132 213 constexpr VkMemoryPropertyFlags kHost =
133 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
134 try {
135
1/1
✓ Branch 1 taken 213 times.
213 return MemoryType(memory, bits, kHost | VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
136
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const std::runtime_error&) {
137
0/1
✗ Branch 1 not taken.
✗ return MemoryType(memory, bits, kHost);
138 }
139 }
140
141 /// Frees `buffer` and resets it, which may be partly created.
142 213 void FreeBuffer(VkDevice device, Buffer& buffer) {
143
1/2
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
213 if (buffer.mapped != nullptr) {
144 213 vkUnmapMemory(device, buffer.memory);
145 }
146
1/2
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
213 if (buffer.memory != VK_NULL_HANDLE) {
147 213 vkFreeMemory(device, buffer.memory, nullptr);
148 }
149
1/2
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
213 if (buffer.buffer != VK_NULL_HANDLE) {
150 213 vkDestroyBuffer(device, buffer.buffer, nullptr);
151 }
152 213 buffer = {};
153 213 }
154
155 /// Creates a mapped, host-coherent storage buffer of `size` bytes into `buffer`, which the caller
156 /// frees even when this throws.
157 213 void MakeBuffer(VkDevice device, const VkPhysicalDeviceMemoryProperties& memory, std::size_t size,
158 Buffer& buffer) {
159 213 VkBufferCreateInfo info{};
160 213 info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
161 213 info.size = std::max<std::size_t>(size, 4);
162 213 info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
163 213 info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
164
2/2
✓ Branch 1 taken 213 times.
✓ Branch 4 taken 213 times.
213 Check(vkCreateBuffer(device, &info, nullptr, &buffer.buffer), "buffer creation");
165 VkMemoryRequirements requirements;
166
1/1
✓ Branch 1 taken 213 times.
213 vkGetBufferMemoryRequirements(device, buffer.buffer, &requirements);
167 213 VkMemoryAllocateInfo allocate{};
168 213 allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
169 213 allocate.allocationSize = requirements.size;
170
1/1
✓ Branch 1 taken 213 times.
213 allocate.memoryTypeIndex = HostMemoryType(memory, requirements.memoryTypeBits);
171
2/2
✓ Branch 1 taken 213 times.
✓ Branch 4 taken 213 times.
213 Check(vkAllocateMemory(device, &allocate, nullptr, &buffer.memory), "memory allocation");
172
2/2
✓ Branch 1 taken 213 times.
✓ Branch 4 taken 213 times.
213 Check(vkBindBufferMemory(device, buffer.buffer, buffer.memory, 0), "memory binding");
173
2/2
✓ Branch 1 taken 213 times.
✓ Branch 4 taken 213 times.
213 Check(vkMapMemory(device, buffer.memory, 0, VK_WHOLE_SIZE, 0, &buffer.mapped),
174 "memory mapping");
175 213 }
176
177 /// The kind of a device the driver reports as `type`.
178 ✗ DeviceKind ToDeviceKind(VkPhysicalDeviceType type) {
179
0/5
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ switch (type) {
180 ✗ case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
181 ✗ return DeviceKind::Discrete;
182 ✗ case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
183 ✗ return DeviceKind::Integrated;
184 ✗ case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
185 ✗ return DeviceKind::Virtual;
186 ✗ case VK_PHYSICAL_DEVICE_TYPE_CPU:
187 ✗ return DeviceKind::Cpu;
188 ✗ default:
189 ✗ return DeviceKind::Other;
190 }
191 }
192
193 /// Opens the Vulkan loader and creates an instance with its functions loaded.
194 21 VkInstance CreateInstance() {
195
2/3
✓ Branch 1 taken 21 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
21 if (volkInitialize() != VK_SUCCESS) {
196
0/3
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✗ throw std::runtime_error("no Vulkan loader found; " + VulkanInstallHint());
197 }
198 21 VkApplicationInfo app{};
199 21 app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
200 21 app.pApplicationName = "TerrainGen";
201 21 app.apiVersion = VK_API_VERSION_1_1;
202 21 VkInstanceCreateInfo instanceInfo{};
203 21 instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
204 21 instanceInfo.pApplicationInfo = &app;
205 #ifdef __APPLE__
206 const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME;
207 instanceInfo.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
208 instanceInfo.enabledExtensionCount = 1;
209 instanceInfo.ppEnabledExtensionNames = &portability;
210 #endif
211 21 VkInstance instance = VK_NULL_HANDLE;
212
1/1
✓ Branch 1 taken 21 times.
21 const VkResult created = vkCreateInstance(&instanceInfo, nullptr, &instance);
213
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21 times.
21 if (created == VK_ERROR_INCOMPATIBLE_DRIVER) {
214
0/3
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✗ throw std::runtime_error("no Vulkan driver found; " + VulkanInstallHint());
215 }
216
1/1
✓ Branch 1 taken 21 times.
21 Check(created, "instance creation");
217
1/1
✓ Branch 1 taken 21 times.
21 volkLoadInstance(instance);
218 21 return instance;
219 }
220
221 /// The device `TERRAINGEN_DEVICE` names, otherwise the one `PickDevice` picks.
222 21 VkPhysicalDevice ChooseDevice(VkInstance instance) {
223 21 std::uint32_t count = 0;
224
1/1
✓ Branch 1 taken 21 times.
21 vkEnumeratePhysicalDevices(instance, &count, nullptr);
225
1/1
✓ Branch 1 taken 21 times.
21 std::vector<VkPhysicalDevice> devices(count);
226
1/1
✓ Branch 2 taken 21 times.
21 vkEnumeratePhysicalDevices(instance, &count, devices.data());
227
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
21 if (devices.empty()) {
228
0/3
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✗ throw std::runtime_error("no Vulkan devices; " + VulkanInstallHint());
229 }
230
2/4
✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
✗ Branch 4 not taken.
21 if (const char* wanted = std::getenv("TERRAINGEN_DEVICE"); wanted && *wanted) {
231 21 const auto chosen = static_cast<std::size_t>(std::strtoul(wanted, nullptr, 10));
232
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
21 if (chosen >= devices.size()) {
233
0/4
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 7 not taken.
✗ Branch 10 not taken.
✗ throw std::runtime_error("TERRAINGEN_DEVICE " + std::string(wanted) + " is past the " +
234
0/3
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ std::to_string(devices.size()) + " Vulkan devices");
235 }
236 21 return devices[chosen];
237 }
238
0/1
✗ Branch 2 not taken.
✗ std::vector<DeviceKind> kinds(devices.size());
239 ✗ std::string names;
240
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ for (std::size_t i = 0; i < devices.size(); ++i) {
241 VkPhysicalDeviceProperties properties;
242
0/1
✗ Branch 2 not taken.
✗ vkGetPhysicalDeviceProperties(devices[i], &properties);
243 ✗ kinds[i] = ToDeviceKind(properties.deviceType);
244
0/5
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ names += (i == 0 ? "" : ", ") + std::string(properties.deviceName);
245 }
246
0/3
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ if (const std::optional<std::size_t> chosen = PickDevice(kinds)) {
247 ✗ return devices[*chosen];
248 }
249
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ throw std::runtime_error("only software Vulkan (" + names +
250 "), which is slower than the CPU backend; set TERRAINGEN_DEVICE to "
251
0/1
✗ Branch 1 not taken.
✗ "its index to use it anyway, or " +
252
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ VulkanInstallHint());
253 21 }
254
255 /// The first queue family of `physical` that supports compute.
256 21 std::uint32_t ComputeQueueFamily(VkPhysicalDevice physical) {
257 21 std::uint32_t families = 0;
258
1/1
✓ Branch 1 taken 21 times.
21 vkGetPhysicalDeviceQueueFamilyProperties(physical, &families, nullptr);
259
1/1
✓ Branch 1 taken 21 times.
21 std::vector<VkQueueFamilyProperties> properties(families);
260
1/1
✓ Branch 2 taken 21 times.
21 vkGetPhysicalDeviceQueueFamilyProperties(physical, &families, properties.data());
261
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 for (std::uint32_t i = 0; i < families; ++i) {
262
1/2
✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
21 if ((properties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0) {
263 21 return i;
264 }
265 }
266
0/1
✗ Branch 2 not taken.
✗ throw std::runtime_error("the Vulkan device has no compute queue");
267 21 }
268
269 /// A logical device with one queue of `family`, with its functions loaded.
270 21 VkDevice CreateDevice(VkPhysicalDevice physical, std::uint32_t family) {
271 21 const float priority = 1.0f;
272 21 VkDeviceQueueCreateInfo queueInfo{};
273 21 queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
274 21 queueInfo.queueFamilyIndex = family;
275 21 queueInfo.queueCount = 1;
276 21 queueInfo.pQueuePriorities = &priority;
277 21 VkDeviceCreateInfo deviceInfo{};
278 21 deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
279 21 deviceInfo.queueCreateInfoCount = 1;
280 21 deviceInfo.pQueueCreateInfos = &queueInfo;
281 21 VkDevice device = VK_NULL_HANDLE;
282
2/2
✓ Branch 1 taken 21 times.
✓ Branch 4 taken 21 times.
21 Check(vkCreateDevice(physical, &deviceInfo, nullptr, &device), "device creation");
283
1/1
✓ Branch 1 taken 21 times.
21 volkLoadDevice(device);
284 21 return device;
285 }
286
287 21 VkCommandPool CreateCommandPool(VkDevice device, std::uint32_t family) {
288 21 VkCommandPoolCreateInfo poolInfo{};
289 21 poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
290 21 poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
291 21 poolInfo.queueFamilyIndex = family;
292 21 VkCommandPool pool = VK_NULL_HANDLE;
293
2/2
✓ Branch 1 taken 21 times.
✓ Branch 4 taken 21 times.
21 Check(vkCreateCommandPool(device, &poolInfo, nullptr, &pool), "command pool");
294 21 return pool;
295 }
296
297 /// The objects of one `Run`, released when it goes out of scope however the run ends.
298 struct RunResources {
299 VkDevice device;
300 VkCommandPool pool;
301 std::vector<Buffer> buffers;
302 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
303 VkCommandBuffer command = VK_NULL_HANDLE;
304 VkFence fence = VK_NULL_HANDLE;
305
306 37 RunResources(VkDevice owner, VkCommandPool commandPool, std::size_t count)
307
1/1
✓ Branch 1 taken 37 times.
111 : device(owner), pool(commandPool), buffers(count) {}
308 RunResources(const RunResources&) = delete;
309 RunResources& operator=(const RunResources&) = delete;
310
311 37 ~RunResources() {
312
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 if (fence != VK_NULL_HANDLE) {
313 37 vkDestroyFence(device, fence, nullptr);
314 }
315
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 if (command != VK_NULL_HANDLE) {
316 37 vkFreeCommandBuffers(device, pool, 1, &command);
317 }
318
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 if (descriptorPool != VK_NULL_HANDLE) {
319 37 vkDestroyDescriptorPool(device, descriptorPool, nullptr);
320 }
321
2/2
✓ Branch 5 taken 213 times.
✓ Branch 6 taken 37 times.
250 for (Buffer& buffer : buffers) {
322 213 FreeBuffer(device, buffer);
323 }
324 37 }
325 };
326
327 /// Creates a device buffer for each host buffer and copies the host data into it.
328 37 void UploadBuffers(const std::vector<std::span<std::byte>>& host,
329 const VkPhysicalDeviceMemoryProperties& memory, RunResources& run) {
330
2/2
✓ Branch 1 taken 213 times.
✓ Branch 2 taken 37 times.
250 for (std::size_t i = 0; i < host.size(); ++i) {
331 213 MakeBuffer(run.device, memory, host[i].size(), run.buffers[i]);
332 213 std::memcpy(run.buffers[i].mapped, host[i].data(), host[i].size());
333 }
334 37 }
335
336 /// A descriptor set binding the run's buffers as storage buffers 0, 1, ...
337 37 VkDescriptorSet BindBuffers(const Pipeline& pipeline, RunResources& run) {
338 37 VkDescriptorPoolSize poolSize{
339 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
340 37 static_cast<std::uint32_t>(std::max<std::size_t>(run.buffers.size(), 1))};
341 37 VkDescriptorPoolCreateInfo poolInfo{};
342 37 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
343 37 poolInfo.maxSets = 1;
344 37 poolInfo.poolSizeCount = 1;
345 37 poolInfo.pPoolSizes = &poolSize;
346
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkCreateDescriptorPool(run.device, &poolInfo, nullptr, &run.descriptorPool),
347 "descriptor pool");
348 37 VkDescriptorSetAllocateInfo setInfo{};
349 37 setInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
350 37 setInfo.descriptorPool = run.descriptorPool;
351 37 setInfo.descriptorSetCount = 1;
352 37 setInfo.pSetLayouts = &pipeline.setLayout;
353 37 VkDescriptorSet set = VK_NULL_HANDLE;
354
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkAllocateDescriptorSets(run.device, &setInfo, &set), "descriptor set");
355
1/1
✓ Branch 2 taken 37 times.
74 std::vector<VkDescriptorBufferInfo> infos(run.buffers.size());
356
1/1
✓ Branch 2 taken 37 times.
37 std::vector<VkWriteDescriptorSet> writes(run.buffers.size());
357
2/2
✓ Branch 1 taken 213 times.
✓ Branch 2 taken 37 times.
250 for (std::size_t i = 0; i < run.buffers.size(); ++i) {
358 213 infos[i] = {run.buffers[i].buffer, 0, VK_WHOLE_SIZE};
359 213 writes[i] = {};
360 213 writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
361 213 writes[i].dstSet = set;
362 213 writes[i].dstBinding = static_cast<std::uint32_t>(i);
363 213 writes[i].descriptorCount = 1;
364 213 writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
365 213 writes[i].pBufferInfo = &infos[i];
366 }
367
1/1
✓ Branch 3 taken 37 times.
37 vkUpdateDescriptorSets(run.device, static_cast<std::uint32_t>(writes.size()), writes.data(), 0,
368 nullptr);
369 37 return set;
370 37 }
371
372 /// Records the dispatch, then a barrier making its writes visible to the host.
373 37 void RecordDispatch(const Pipeline& pipeline, VkDescriptorSet set,
374 std::span<const std::byte> pushConstants, std::array<std::uint32_t, 3> groups,
375 RunResources& run) {
376 37 VkCommandBufferAllocateInfo commandInfo{};
377 37 commandInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
378 37 commandInfo.commandPool = run.pool;
379 37 commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
380 37 commandInfo.commandBufferCount = 1;
381
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkAllocateCommandBuffers(run.device, &commandInfo, &run.command), "command buffer");
382 37 VkCommandBufferBeginInfo begin{};
383 37 begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
384 37 begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
385
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkBeginCommandBuffer(run.command, &begin), "command recording");
386
1/1
✓ Branch 1 taken 37 times.
37 vkCmdBindPipeline(run.command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.pipeline);
387
1/1
✓ Branch 1 taken 37 times.
37 vkCmdBindDescriptorSets(run.command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.layout, 0, 1,
388 &set, 0, nullptr);
389
1/2
✓ Branch 1 taken 37 times.
✗ Branch 2 not taken.
37 if (!pushConstants.empty()) {
390
1/1
✓ Branch 1 taken 37 times.
37 vkCmdPushConstants(run.command, pipeline.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0,
391 37 static_cast<std::uint32_t>(pushConstants.size()), pushConstants.data());
392 }
393
1/1
✓ Branch 4 taken 37 times.
37 vkCmdDispatch(run.command, groups[0], groups[1], groups[2]);
394 37 VkMemoryBarrier barrier{};
395 37 barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
396 37 barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
397 37 barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
398
1/1
✓ Branch 1 taken 37 times.
37 vkCmdPipelineBarrier(run.command, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
399 VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
400
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkEndCommandBuffer(run.command), "command recording");
401 37 }
402
403 37 void SubmitAndWait(VkQueue queue, RunResources& run) {
404 37 VkFenceCreateInfo fenceInfo{};
405 37 fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
406
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkCreateFence(run.device, &fenceInfo, nullptr, &run.fence), "fence");
407 37 VkSubmitInfo submit{};
408 37 submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
409 37 submit.commandBufferCount = 1;
410 37 submit.pCommandBuffers = &run.command;
411
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkQueueSubmit(queue, 1, &submit, run.fence), "submission");
412
2/2
✓ Branch 1 taken 37 times.
✓ Branch 4 taken 37 times.
37 Check(vkWaitForFences(run.device, 1, &run.fence, VK_TRUE, UINT64_MAX), "wait");
413 37 }
414
415 37 void DownloadBuffers(const RunResources& run, const std::vector<std::span<std::byte>>& host) {
416
2/2
✓ Branch 1 taken 213 times.
✓ Branch 2 taken 37 times.
250 for (std::size_t i = 0; i < host.size(); ++i) {
417 213 std::memcpy(host[i].data(), run.buffers[i].mapped, host[i].size());
418 }
419 37 }
420
421 } // namespace
422
423 /// The instance, device and cached pipelines; destroys whatever was created, so a partly opened
424 /// device is released as a fully opened one is.
425 struct VulkanCompute::State {
426 VkInstance instance = VK_NULL_HANDLE;
427 VkPhysicalDevice physical = VK_NULL_HANDLE;
428 VkDevice device = VK_NULL_HANDLE;
429 VkQueue queue = VK_NULL_HANDLE;
430 std::uint32_t family = 0;
431 VkCommandPool pool = VK_NULL_HANDLE;
432 VkPhysicalDeviceMemoryProperties memory{};
433 std::uint32_t maxPushConstants = 0;
434 std::string name;
435 std::mutex mutex;
436 /// Pipelines keyed by kernel code, binding count and push-constant size, which all shape the
437 /// pipeline layout.
438 std::map<std::tuple<const unsigned char*, std::size_t, std::size_t>, Pipeline> pipelines;
439
440 21 State() = default;
441 State(const State&) = delete;
442 State& operator=(const State&) = delete;
443
444 ✗ ~State() {
445
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (device != VK_NULL_HANDLE) {
446 ✗ vkDeviceWaitIdle(device);
447
0/2
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ for (const auto& [key, p] : pipelines) {
448 ✗ DestroyPipeline(device, p);
449 }
450
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (pool != VK_NULL_HANDLE) {
451 ✗ vkDestroyCommandPool(device, pool, nullptr);
452 }
453 ✗ vkDestroyDevice(device, nullptr);
454 }
455
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (instance != VK_NULL_HANDLE) {
456 ✗ vkDestroyInstance(instance, nullptr);
457 }
458 }
459
460 /// Opens the device: instance, physical device, queue, logical device and command pool.
461 21 void Open() {
462
1/1
✓ Branch 1 taken 21 times.
21 instance = CreateInstance();
463
1/1
✓ Branch 1 taken 21 times.
21 physical = ChooseDevice(instance);
464 VkPhysicalDeviceProperties properties;
465
1/1
✓ Branch 1 taken 21 times.
21 vkGetPhysicalDeviceProperties(physical, &properties);
466
1/1
✓ Branch 1 taken 21 times.
21 name = properties.deviceName;
467 21 maxPushConstants = properties.limits.maxPushConstantsSize;
468
1/1
✓ Branch 1 taken 21 times.
21 vkGetPhysicalDeviceMemoryProperties(physical, &memory);
469
1/1
✓ Branch 1 taken 21 times.
21 family = ComputeQueueFamily(physical);
470
1/1
✓ Branch 1 taken 21 times.
21 device = CreateDevice(physical, family);
471
1/1
✓ Branch 1 taken 21 times.
21 vkGetDeviceQueue(device, family, 0, &queue);
472
1/1
✓ Branch 1 taken 21 times.
21 pool = CreateCommandPool(device, family);
473 21 }
474
475 37 const Pipeline& PipelineFor(const ComputeKernel& kernel, std::size_t bindings,
476 std::size_t pushSize) {
477
1/1
✓ Branch 1 taken 37 times.
37 const auto key = std::make_tuple(kernel.spirv, bindings, pushSize);
478
1/1
✓ Branch 1 taken 37 times.
37 auto found = pipelines.find(key);
479
2/2
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 20 times.
37 if (found != pipelines.end()) {
480 17 return found->second;
481 }
482
2/2
✓ Branch 1 taken 20 times.
✓ Branch 4 taken 20 times.
40 return pipelines.emplace(key, CreatePipeline(device, kernel, bindings, pushSize))
483 20 .first->second;
484 }
485 };
486
487 namespace {
488
489 std::once_flag gOnce;
490 VulkanCompute* gCompute = nullptr;
491 std::string gReason;
492
493 } // namespace
494
495 6 std::optional<std::size_t> PickDevice(std::span<const DeviceKind> kinds) {
496 20 for (const DeviceKind kind :
497
2/2
✓ Branch 2 taken 18 times.
✓ Branch 3 taken 2 times.
26 {DeviceKind::Discrete, DeviceKind::Integrated, DeviceKind::Virtual, DeviceKind::Other}) {
498
1/1
✓ Branch 3 taken 18 times.
18 const auto found = std::find(kinds.begin(), kinds.end(), kind);
499
2/2
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 14 times.
18 if (found != kinds.end()) {
500 4 return static_cast<std::size_t>(found - kinds.begin());
501 }
502 }
503 2 return std::nullopt;
504 }
505
506 1 std::string VulkanInstallHint() {
507 #if defined(__APPLE__)
508 return "install MoltenVK (brew install molten-vk) or the Vulkan SDK from vulkan.lunarg.com";
509 #elif defined(_WIN32)
510 return "install or update the GPU driver, which provides the Vulkan loader (vulkan-1.dll)";
511 #else
512 return "install the Vulkan loader and a GPU driver (on Debian and Ubuntu: sudo apt install "
513 "libvulkan1, with the NVIDIA driver or mesa-vulkan-drivers); in Docker on an NVIDIA "
514
1/1
✓ Branch 1 taken 1 time.
2 "GPU, include graphics in NVIDIA_DRIVER_CAPABILITIES";
515 #endif
516 }
517
518 21 VulkanCompute::VulkanCompute(std::unique_ptr<State> state) : state_(std::move(state)) {}
519
520 ✗ VulkanCompute::~VulkanCompute() = default;
521
522 117 VulkanCompute* VulkanCompute::Get() {
523
1/1
✓ Branch 1 taken 117 times.
117 std::call_once(gOnce, [] {
524 try {
525
1/1
✓ Branch 1 taken 21 times.
21 auto state = std::make_unique<State>();
526
1/1
✓ Branch 2 taken 21 times.
21 state->Open();
527
1/1
✓ Branch 2 taken 21 times.
21 const std::string name = state->name;
528
1/1
✓ Branch 1 taken 21 times.
21 gCompute = new VulkanCompute(std::move(state));
529
1/1
✓ Branch 2 taken 21 times.
21 LogInfo("gpu", "Vulkan compute on ", name);
530
0/2
✗ Branch 4 not taken.
✗ Branch 5 not taken.
21 } catch (const std::exception& error) {
531
0/1
✗ Branch 2 not taken.
✗ gReason = error.what();
532
0/1
✗ Branch 2 not taken.
✗ LogInfo("gpu", "Vulkan unavailable: ", gReason);
533 }
534 21 });
535 117 return gCompute;
536 }
537
538 38 std::string VulkanCompute::Reason() {
539 38 Get();
540 38 return gReason;
541 }
542
543 39 const std::string& VulkanCompute::DeviceName() const {
544 39 return state_->name;
545 }
546
547 38 void VulkanCompute::Run(const ComputeKernel& kernel,
548 const std::vector<std::span<std::byte>>& buffers,
549 std::span<const std::byte> pushConstants,
550 std::array<std::uint32_t, 3> groups) {
551 38 State& s = *state_;
552
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 37 times.
38 if (pushConstants.size() > s.maxPushConstants) {
553
1/1
✓ Branch 2 taken 1 time.
1 throw std::runtime_error("Push constants exceed the device's limit");
554 }
555
1/1
✓ Branch 1 taken 37 times.
37 const std::lock_guard lock(s.mutex);
556
1/1
✓ Branch 3 taken 37 times.
37 const Pipeline& pipeline = s.PipelineFor(kernel, buffers.size(), pushConstants.size());
557
1/1
✓ Branch 2 taken 37 times.
37 RunResources run(s.device, s.pool, buffers.size());
558
1/1
✓ Branch 1 taken 37 times.
37 UploadBuffers(buffers, s.memory, run);
559
1/1
✓ Branch 1 taken 37 times.
37 const VkDescriptorSet set = BindBuffers(pipeline, run);
560
1/1
✓ Branch 1 taken 37 times.
37 RecordDispatch(pipeline, set, pushConstants, groups, run);
561
1/1
✓ Branch 1 taken 37 times.
37 SubmitAndWait(s.queue, run);
562 37 DownloadBuffers(run, buffers);
563 37 }
564
565 } // namespace terraingen
566