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 | 1291 | if (result != VK_SUCCESS) { | |
| 23 | ✗ | throw std::runtime_error(std::string("Vulkan ") + what + " failed (" + | |
| 24 | ✗ | 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 | 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 | 20 | std::vector<VkDescriptorSetLayoutBinding> layoutBindings(bindings); | |
| 57 | 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 | 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 | 20 | info.pushConstantRangeCount = pushSize > 0 ? 1u : 0u; | |
| 79 | 20 | info.pPushConstantRanges = ⦥ | |
| 80 | 20 | VkPipelineLayout layout = VK_NULL_HANDLE; | |
| 81 | 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 | 20 | p.module = CreateShaderModule(device, kernel); | |
| 92 | 20 | p.setLayout = CreateSetLayout(device, bindings); | |
| 93 | 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 | 20 | Check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &p.pipeline), | |
| 102 | "compute pipeline"); | ||
| 103 | ✗ | } catch (...) { | |
| 104 | ✗ | 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 | 213 | for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { | |
| 121 | 213 | if ((bits & (1u << i)) != 0 && (memory.memoryTypes[i].propertyFlags & flags) == flags) { | |
| 122 | 213 | return i; | |
| 123 | } | ||
| 124 | } | ||
| 125 | ✗ | 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 | 213 | return MemoryType(memory, bits, kHost | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); | |
| 136 | ✗ | } catch (const std::runtime_error&) { | |
| 137 | ✗ | 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 | 213 | if (buffer.mapped != nullptr) { | |
| 144 | 213 | vkUnmapMemory(device, buffer.memory); | |
| 145 | } | ||
| 146 | 213 | if (buffer.memory != VK_NULL_HANDLE) { | |
| 147 | 213 | vkFreeMemory(device, buffer.memory, nullptr); | |
| 148 | } | ||
| 149 | 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 | 213 | Check(vkCreateBuffer(device, &info, nullptr, &buffer.buffer), "buffer creation"); | |
| 165 | VkMemoryRequirements requirements; | ||
| 166 | 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 | 213 | allocate.memoryTypeIndex = HostMemoryType(memory, requirements.memoryTypeBits); | |
| 171 | 213 | Check(vkAllocateMemory(device, &allocate, nullptr, &buffer.memory), "memory allocation"); | |
| 172 | 213 | Check(vkBindBufferMemory(device, buffer.buffer, buffer.memory, 0), "memory binding"); | |
| 173 | 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 | ✗ | 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 | 21 | if (volkInitialize() != VK_SUCCESS) { | |
| 196 | ✗ | 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 | 21 | const VkResult created = vkCreateInstance(&instanceInfo, nullptr, &instance); | |
| 213 | 21 | if (created == VK_ERROR_INCOMPATIBLE_DRIVER) { | |
| 214 | ✗ | throw std::runtime_error("no Vulkan driver found; " + VulkanInstallHint()); | |
| 215 | } | ||
| 216 | 21 | Check(created, "instance creation"); | |
| 217 | 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 | 21 | vkEnumeratePhysicalDevices(instance, &count, nullptr); | |
| 225 | 21 | std::vector<VkPhysicalDevice> devices(count); | |
| 226 | 21 | vkEnumeratePhysicalDevices(instance, &count, devices.data()); | |
| 227 | 21 | if (devices.empty()) { | |
| 228 | ✗ | throw std::runtime_error("no Vulkan devices; " + VulkanInstallHint()); | |
| 229 | } | ||
| 230 | 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 | 21 | if (chosen >= devices.size()) { | |
| 233 | ✗ | throw std::runtime_error("TERRAINGEN_DEVICE " + std::string(wanted) + " is past the " + | |
| 234 | ✗ | std::to_string(devices.size()) + " Vulkan devices"); | |
| 235 | } | ||
| 236 | 21 | return devices[chosen]; | |
| 237 | } | ||
| 238 | ✗ | std::vector<DeviceKind> kinds(devices.size()); | |
| 239 | ✗ | std::string names; | |
| 240 | ✗ | for (std::size_t i = 0; i < devices.size(); ++i) { | |
| 241 | VkPhysicalDeviceProperties properties; | ||
| 242 | ✗ | vkGetPhysicalDeviceProperties(devices[i], &properties); | |
| 243 | ✗ | kinds[i] = ToDeviceKind(properties.deviceType); | |
| 244 | ✗ | names += (i == 0 ? "" : ", ") + std::string(properties.deviceName); | |
| 245 | } | ||
| 246 | ✗ | if (const std::optional<std::size_t> chosen = PickDevice(kinds)) { | |
| 247 | ✗ | return devices[*chosen]; | |
| 248 | } | ||
| 249 | ✗ | throw std::runtime_error("only software Vulkan (" + names + | |
| 250 | "), which is slower than the CPU backend; set TERRAINGEN_DEVICE to " | ||
| 251 | ✗ | "its index to use it anyway, or " + | |
| 252 | ✗ | 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 | 21 | vkGetPhysicalDeviceQueueFamilyProperties(physical, &families, nullptr); | |
| 259 | 21 | std::vector<VkQueueFamilyProperties> properties(families); | |
| 260 | 21 | vkGetPhysicalDeviceQueueFamilyProperties(physical, &families, properties.data()); | |
| 261 | 21 | for (std::uint32_t i = 0; i < families; ++i) { | |
| 262 | 21 | if ((properties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0) { | |
| 263 | 21 | return i; | |
| 264 | } | ||
| 265 | } | ||
| 266 | ✗ | 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 | 21 | Check(vkCreateDevice(physical, &deviceInfo, nullptr, &device), "device creation"); | |
| 283 | 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 | 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 | 111 | : device(owner), pool(commandPool), buffers(count) {} | |
| 308 | RunResources(const RunResources&) = delete; | ||
| 309 | RunResources& operator=(const RunResources&) = delete; | ||
| 310 | |||
| 311 | 37 | ~RunResources() { | |
| 312 | 37 | if (fence != VK_NULL_HANDLE) { | |
| 313 | 37 | vkDestroyFence(device, fence, nullptr); | |
| 314 | } | ||
| 315 | 37 | if (command != VK_NULL_HANDLE) { | |
| 316 | 37 | vkFreeCommandBuffers(device, pool, 1, &command); | |
| 317 | } | ||
| 318 | 37 | if (descriptorPool != VK_NULL_HANDLE) { | |
| 319 | 37 | vkDestroyDescriptorPool(device, descriptorPool, nullptr); | |
| 320 | } | ||
| 321 | 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 | 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 | 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 | 37 | Check(vkAllocateDescriptorSets(run.device, &setInfo, &set), "descriptor set"); | |
| 355 | 74 | std::vector<VkDescriptorBufferInfo> infos(run.buffers.size()); | |
| 356 | 37 | std::vector<VkWriteDescriptorSet> writes(run.buffers.size()); | |
| 357 | 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 | 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 | 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 | 37 | Check(vkBeginCommandBuffer(run.command, &begin), "command recording"); | |
| 386 | 37 | vkCmdBindPipeline(run.command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.pipeline); | |
| 387 | 37 | vkCmdBindDescriptorSets(run.command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.layout, 0, 1, | |
| 388 | &set, 0, nullptr); | ||
| 389 | 37 | if (!pushConstants.empty()) { | |
| 390 | 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 | 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 | 37 | vkCmdPipelineBarrier(run.command, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, | |
| 399 | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); | ||
| 400 | 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 | 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 | 37 | Check(vkQueueSubmit(queue, 1, &submit, run.fence), "submission"); | |
| 412 | 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 | 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 | ✗ | if (device != VK_NULL_HANDLE) { | |
| 446 | ✗ | vkDeviceWaitIdle(device); | |
| 447 | ✗ | for (const auto& [key, p] : pipelines) { | |
| 448 | ✗ | DestroyPipeline(device, p); | |
| 449 | } | ||
| 450 | ✗ | if (pool != VK_NULL_HANDLE) { | |
| 451 | ✗ | vkDestroyCommandPool(device, pool, nullptr); | |
| 452 | } | ||
| 453 | ✗ | vkDestroyDevice(device, nullptr); | |
| 454 | } | ||
| 455 | ✗ | 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 | 21 | instance = CreateInstance(); | |
| 463 | 21 | physical = ChooseDevice(instance); | |
| 464 | VkPhysicalDeviceProperties properties; | ||
| 465 | 21 | vkGetPhysicalDeviceProperties(physical, &properties); | |
| 466 | 21 | name = properties.deviceName; | |
| 467 | 21 | maxPushConstants = properties.limits.maxPushConstantsSize; | |
| 468 | 21 | vkGetPhysicalDeviceMemoryProperties(physical, &memory); | |
| 469 | 21 | family = ComputeQueueFamily(physical); | |
| 470 | 21 | device = CreateDevice(physical, family); | |
| 471 | 21 | vkGetDeviceQueue(device, family, 0, &queue); | |
| 472 | 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 | 37 | const auto key = std::make_tuple(kernel.spirv, bindings, pushSize); | |
| 478 | 37 | auto found = pipelines.find(key); | |
| 479 | 37 | if (found != pipelines.end()) { | |
| 480 | 17 | return found->second; | |
| 481 | } | ||
| 482 | 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 | 26 | {DeviceKind::Discrete, DeviceKind::Integrated, DeviceKind::Virtual, DeviceKind::Other}) { | |
| 498 | 18 | const auto found = std::find(kinds.begin(), kinds.end(), kind); | |
| 499 | 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 | 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 | 117 | std::call_once(gOnce, [] { | |
| 524 | try { | ||
| 525 | 21 | auto state = std::make_unique<State>(); | |
| 526 | 21 | state->Open(); | |
| 527 | 21 | const std::string name = state->name; | |
| 528 | 21 | gCompute = new VulkanCompute(std::move(state)); | |
| 529 | 21 | LogInfo("gpu", "Vulkan compute on ", name); | |
| 530 | 21 | } catch (const std::exception& error) { | |
| 531 | ✗ | gReason = error.what(); | |
| 532 | ✗ | 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 | 38 | if (pushConstants.size() > s.maxPushConstants) { | |
| 553 | 1 | throw std::runtime_error("Push constants exceed the device's limit"); | |
| 554 | } | ||
| 555 | 37 | const std::lock_guard lock(s.mutex); | |
| 556 | 37 | const Pipeline& pipeline = s.PipelineFor(kernel, buffers.size(), pushConstants.size()); | |
| 557 | 37 | RunResources run(s.device, s.pool, buffers.size()); | |
| 558 | 37 | UploadBuffers(buffers, s.memory, run); | |
| 559 | 37 | const VkDescriptorSet set = BindBuffers(pipeline, run); | |
| 560 | 37 | RecordDispatch(pipeline, set, pushConstants, groups, run); | |
| 561 | 37 | SubmitAndWait(s.queue, run); | |
| 562 | 37 | DownloadBuffers(run, buffers); | |
| 563 | 37 | } | |
| 564 | |||
| 565 | } // namespace terraingen | ||
| 566 |