module; #include #include #include #include #include "shaders_embedded.hpp" export module ra3.vulkan; import std; export import ra3.core; import ra3.render; import ra3.terrain; import ra3.client; /** * A Vulkan presentation backend for the engine's rendered scene. * * Red Alert 3 shipped a Direct3D 9 renderer; OpenRA3 replaces the graphics * stack with Vulkan while keeping the platform/window layer on SDL3. This is one * implementation of `ra3::client::display`: the interactive loops live in the * base class, so only the low-level primitives (`init` / `present` / * `poll_event` / `window_size` / `key_down` / `present_terrain` / `shutdown`) * are backend-specific. * * The image pipeline is deliberately small: one graphics pipeline draws a * fullscreen triangle sampling a software-rendered `ra3::render::image`. The * heightfield raymarcher is a second pipeline behind `present_terrain`. A thin * RHI/render-graph will replace the hand-rolled pipelines as materials, skybox * and HUD grow. */ export namespace ra3::vulkan { namespace detail { [[nodiscard]] inline auto check(VkResult result, std::string_view what) -> bool { if (result == VK_SUCCESS) return true; std::cerr << "ra3.vulkan: " << what << " failed (VkResult " << static_cast(result) << ")\n"; return false; } } /** * The Vulkan device, swapchain and pipelines, plus the SDL window and * surface they render to. */ class vulkan_display final : public ra3::client::display { public: vulkan_display() = default; vulkan_display(const vulkan_display &) = delete; auto operator=(const vulkan_display &) -> vulkan_display & = delete; ~vulkan_display() override { this->cleanup(); } /** * Create the window, Vulkan device and swapchain. * * @return false when Vulkan or a display is unavailable; the caller * should then fall back to another display. */ [[nodiscard]] auto init(const ra3::client::display_options &options) -> bool override { if (volkInitialize() != VK_SUCCESS) return false; if (!SDL_Init(SDL_INIT_VIDEO)) return false; this->fps_limit_ = options.fps_limit; window_ = SDL_CreateWindow(options.title.c_str(), options.width, options.height, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | (options.fullscreen ? SDL_WINDOW_FULLSCREEN : 0)); if (window_ == nullptr) return false; SDL_RaiseWindow(window_); // take input focus so the wheel zooms without a click Uint32 extension_count = 0; const char *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extension_count); if (extensions == nullptr) return false; VkApplicationInfo app{}; app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; app.pApplicationName = "OpenRA3"; app.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo instance_info{}; instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; instance_info.pApplicationInfo = &app; instance_info.enabledExtensionCount = extension_count; instance_info.ppEnabledExtensionNames = extensions; if (!detail::check(vkCreateInstance(&instance_info, nullptr, &instance_), "vkCreateInstance")) return false; volkLoadInstance(instance_); if (!SDL_Vulkan_CreateSurface(window_, instance_, nullptr, &surface_)) return false; if (!this->pick_device()) return false; if (!this->create_device()) return false; if (!this->create_swapchain()) return false; if (!this->create_render_pass()) return false; if (!this->create_framebuffers()) return false; if (!this->create_pipeline()) return false; if (!this->create_commands()) return false; if (!this->create_sync()) return false; SDL_StartTextInput(window_); return true; } /** Upload (or replace) the scene texture. */ [[nodiscard]] auto upload(const ra3::render::image &scene) -> bool { if (scene.empty()) return false; const auto width = scene.width(); const auto height = scene.height(); const VkDeviceSize bytes = static_cast(width) * height * 4U; // Fast path: same size -> just refresh the pixels into the existing // image. Recreating the image/view/sampler/descriptor every frame is // what made the menu (which repaints often) stutter and flicker. if (texture_ != VK_NULL_HANDLE && width == texture_width_ && height == texture_height_) { VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_memory = VK_NULL_HANDLE; VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; buffer_info.size = bytes; buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer(scene)")) return false; VkMemoryRequirements br{}; vkGetBufferMemoryRequirements(device_, staging, &br); VkMemoryAllocateInfo ba{}; ba.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; ba.allocationSize = br.size; ba.memoryTypeIndex = this->memory_type(br.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (!detail::check(vkAllocateMemory(device_, &ba, nullptr, &staging_memory), "vkAllocateMemory(scene)")) { vkDestroyBuffer(device_, staging, nullptr); return false; } vkBindBufferMemory(device_, staging, staging_memory, 0); void *mapped = nullptr; vkMapMemory(device_, staging_memory, 0, bytes, 0, &mapped); std::memcpy(mapped, scene.data(), static_cast(bytes)); vkUnmapMemory(device_, staging_memory); this->immediate([&](VkCommandBuffer cmd) { this->transition(cmd, texture_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, 1U}; region.imageExtent = {width, height, 1U}; vkCmdCopyBufferToImage(cmd, staging, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, ®ion); this->transition(cmd, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); }); vkDestroyBuffer(device_, staging, nullptr); vkFreeMemory(device_, staging_memory, nullptr); return true; } this->destroy_texture(); VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; buffer_info.size = bytes; buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_memory = VK_NULL_HANDLE; if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer")) return false; VkMemoryRequirements requirements{}; vkGetBufferMemoryRequirements(device_, staging, &requirements); VkMemoryAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &staging_memory), "vkAllocateMemory(staging)")) return false; if (!detail::check(vkBindBufferMemory(device_, staging, staging_memory, 0), "vkBindBufferMemory")) return false; void *mapped = nullptr; if (!detail::check(vkMapMemory(device_, staging_memory, 0, bytes, 0, &mapped), "vkMapMemory")) return false; std::memcpy(mapped, scene.data(), static_cast(bytes)); vkUnmapMemory(device_, staging_memory); // VK_FORMAT_B8G8R8A8_UNORM matches the software image's 0xAARRGGBB. VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {width, height, 1U}; image_info.mipLevels = 1U; image_info.arrayLayers = 1U; image_info.format = VK_FORMAT_B8G8R8A8_UNORM; image_info.tiling = VK_IMAGE_TILING_OPTIMAL; image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; image_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; image_info.samples = VK_SAMPLE_COUNT_1_BIT; image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (!detail::check(vkCreateImage(device_, &image_info, nullptr, &texture_), "vkCreateImage")) return false; VkMemoryRequirements image_requirements{}; vkGetImageMemoryRequirements(device_, texture_, &image_requirements); VkMemoryAllocateInfo image_allocate{}; image_allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; image_allocate.allocationSize = image_requirements.size; image_allocate.memoryTypeIndex = this->memory_type(image_requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); if (!detail::check(vkAllocateMemory(device_, &image_allocate, nullptr, &texture_memory_), "vkAllocateMemory(texture)")) return false; if (!detail::check(vkBindImageMemory(device_, texture_, texture_memory_, 0), "vkBindImageMemory")) return false; this->immediate([&](VkCommandBuffer cmd) { this->transition(cmd, texture_, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, 1U}; region.imageExtent = {width, height, 1U}; vkCmdCopyBufferToImage(cmd, staging, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, ®ion); this->transition(cmd, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); }); vkDestroyBuffer(device_, staging, nullptr); vkFreeMemory(device_, staging_memory, nullptr); VkImageViewCreateInfo view_info{}; view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view_info.image = texture_; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; view_info.format = VK_FORMAT_B8G8R8A8_UNORM; view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 1U, 0U, 1U}; if (!detail::check(vkCreateImageView(device_, &view_info, nullptr, &texture_view_), "vkCreateImageView")) return false; VkSamplerCreateInfo sampler_info{}; sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; sampler_info.magFilter = VK_FILTER_NEAREST; sampler_info.minFilter = VK_FILTER_NEAREST; sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampler_info.maxLod = 1.0F; if (!detail::check(vkCreateSampler(device_, &sampler_info, nullptr, &sampler_), "vkCreateSampler")) return false; VkDescriptorImageInfo descriptor_image{}; descriptor_image.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; descriptor_image.imageView = texture_view_; descriptor_image.sampler = sampler_; VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; write.dstSet = descriptor_set_; write.dstBinding = 0U; write.descriptorCount = 1U; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &descriptor_image; vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr); texture_width_ = width; texture_height_ = height; return true; } /** * Interactive 3D camera loop: the viewer owns the camera and asks * `provider` to re-render whenever it moves. * * Controls: wheel / +/- move the camera closer or farther (camera * height, not an image scale); WASD or the arrows and screen-edge pushes * pan the target; left-drag rotates yaw/pitch; Q/E yaw; Esc quits. */ [[nodiscard]] auto present(const ra3::render::image &frame, const ra3::render::view_rect &dest, bool changed) -> bool override { if (frame.empty()) return false; if (changed || texture_ == VK_NULL_HANDLE || frame.width() != texture_width_ || frame.height() != texture_height_) { if (!this->upload(frame)) return false; } int w = 0; int h = 0; SDL_GetWindowSizeInPixels(window_, &w, &h); return this->draw(dest, static_cast(w), static_cast(h)); } [[nodiscard]] auto poll_event(ra3::render::ui_event &out) -> bool override { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_EVENT_QUIT: out = {}; out.type = ra3::render::ui_event_type::quit; return true; case SDL_EVENT_KEY_DOWN: if (const auto key = map_key(event.key.key); key != ra3::render::ui_key::none) { out = {}; out.type = ra3::render::ui_event_type::key; out.key = key; return true; } break; case SDL_EVENT_TEXT_INPUT: if (event.text.text[0] != '\0') { out = {}; out.type = ra3::render::ui_event_type::text; out.character = event.text.text[0]; return true; } break; case SDL_EVENT_MOUSE_MOTION: out = {}; out.type = ra3::render::ui_event_type::mouse_move; out.x = event.motion.x; out.y = event.motion.y; out.dx = event.motion.xrel; out.dy = event.motion.yrel; out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U; return true; case SDL_EVENT_MOUSE_BUTTON_DOWN: out = {}; out.type = ra3::render::ui_event_type::mouse_button; out.x = event.button.x; out.y = event.button.y; out.left = event.button.button == SDL_BUTTON_LEFT; return true; case SDL_EVENT_MOUSE_WHEEL: out = {}; out.type = ra3::render::ui_event_type::wheel; out.wheel = event.wheel.y; return true; default: break; } } return false; } [[nodiscard]] auto window_size() const -> std::pair override { int w = 0; int h = 0; SDL_GetWindowSizeInPixels(window_, &w, &h); return {w, h}; } [[nodiscard]] auto key_down(ra3::render::ui_key key) const -> bool override { const bool *keys = SDL_GetKeyboardState(nullptr); if (keys == nullptr) return false; switch (key) { case ra3::render::ui_key::up: return keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]; case ra3::render::ui_key::down: return keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]; case ra3::render::ui_key::left: return keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT]; case ra3::render::ui_key::right: return keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT]; default: return false; } } [[nodiscard]] auto supports_terrain() const -> bool override { return true; } [[nodiscard]] auto present_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s, const ra3::client::terrain_overlay &overlay) -> bool override { if (!terrain_ready_) { if (!this->create_terrain_pipeline()) return false; if (!this->create_terrain_images(terrain)) return false; if (!this->create_terrain_descriptors()) return false; if (!terrain.objects.empty()) { if (!this->create_object_pipeline()) return false; if (!this->create_object_buffers(terrain)) return false; if (!this->create_object_descriptors()) return false; objects_ready_ = true; } terrain_ready_ = true; } if (overlay.label_changed) this->update_overlay(overlay_label_, overlay_label_set_, overlay_label_w_, overlay_label_h_, overlay.label); if (overlay.minimap_changed) this->update_overlay(overlay_minimap_, overlay_minimap_set_, overlay_minimap_w_, overlay_minimap_h_, overlay.minimap); int w = 0; int h = 0; SDL_GetWindowSizeInPixels(window_, &w, &h); const auto aspect = static_cast(w) / static_cast(std::max(1, h)); return this->draw_terrain(terrain, camera, time_s, aspect); } auto shutdown() -> void override { this->cleanup(); } [[nodiscard]] auto name() const -> std::string_view override { return "vulkan"; } private: static constexpr uint32_t frames_in_flight = 2U; [[nodiscard]] static auto map_key(SDL_Keycode key) -> ra3::render::ui_key { using ra3::render::ui_key; switch (key) { case SDLK_UP: return ui_key::up; case SDLK_DOWN: return ui_key::down; case SDLK_LEFT: return ui_key::left; case SDLK_RIGHT: return ui_key::right; case SDLK_PAGEUP: return ui_key::page_up; case SDLK_PAGEDOWN: return ui_key::page_down; case SDLK_RETURN: case SDLK_KP_ENTER: return ui_key::confirm; case SDLK_ESCAPE: return ui_key::cancel; case SDLK_TAB: return ui_key::tab; case SDLK_BACKSPACE: return ui_key::backspace; default: return ui_key::none; } } struct gpu_image { VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkSampler sampler = VK_NULL_HANDLE; uint32_t mips = 1U; uint32_t width = 0U; uint32_t height = 0U; }; /** * Upload an overlay into a **persistent** fixed-size texture (created * once, then only its pixels are refreshed). Reusing the image and its * descriptor set avoids per-frame texture churn (which caused stutter) * and the descriptor-pool growth (which caused corruption). */ auto update_overlay(gpu_image &target, VkDescriptorSet &set, uint32_t &out_w, uint32_t &out_h, const ra3::render::image &source) -> void { if (source.empty()) { out_w = 0; out_h = 0; return; } const auto w = source.width(); const auto h = source.height(); out_w = w; out_h = h; if (target.image == VK_NULL_HANDLE) { const auto bytes = static_cast(w) * h * 4U; if (!this->make_gpu_image(target, w, h, VK_FORMAT_B8G8R8A8_UNORM, source.data(), bytes, 1U, VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST)) return; VkDescriptorSetAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocate.descriptorPool = descriptor_pool_; allocate.descriptorSetCount = 1U; allocate.pSetLayouts = &descriptor_layout_; if (vkAllocateDescriptorSets(device_, &allocate, &set) != VK_SUCCESS) return; VkDescriptorImageInfo info{}; info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; info.imageView = target.view; info.sampler = target.sampler; VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; write.dstSet = set; write.dstBinding = 0U; write.descriptorCount = 1U; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &info; vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr); return; // the creation copy already holds the pixels } if (w > target.width || h > target.height) return; // fixed size, ignore growth const auto bytes = static_cast(w) * h * 4U; VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; buffer_info.size = bytes; buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer(overlay)")) return; VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device_, staging, &req); VkMemoryAllocateInfo alloc{}; alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc.allocationSize = req.size; alloc.memoryTypeIndex = this->memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (!detail::check(vkAllocateMemory(device_, &alloc, nullptr, &memory), "vkAllocateMemory(overlay)")) { vkDestroyBuffer(device_, staging, nullptr); return; } vkBindBufferMemory(device_, staging, memory, 0); void *mapped = nullptr; vkMapMemory(device_, memory, 0, bytes, 0, &mapped); std::memcpy(mapped, source.data(), static_cast(bytes)); vkUnmapMemory(device_, memory); this->immediate([&](VkCommandBuffer cmd) { this->barrier_mip(cmd, target.image, 0U, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, 1U}; region.imageExtent = {w, h, 1U}; vkCmdCopyBufferToImage(cmd, staging, target.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, ®ion); this->barrier_mip(cmd, target.image, 0U, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); }); vkDestroyBuffer(device_, staging, nullptr); vkFreeMemory(device_, memory, nullptr); } auto barrier_mip(VkCommandBuffer cmd, VkImage image, uint32_t level, VkImageLayout from, VkImageLayout to, VkAccessFlags src_access, VkAccessFlags dst_access, uint32_t layer_count = 1U) -> void { VkImageMemoryBarrier barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = from; barrier.newLayout = to; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, level, 1U, 0U, layer_count}; barrier.srcAccessMask = src_access; barrier.dstAccessMask = dst_access; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0U, 0U, nullptr, 0U, nullptr, 1U, &barrier); } [[nodiscard]] auto make_gpu_image(gpu_image &out, uint32_t w, uint32_t h, VkFormat format, const void *data, VkDeviceSize bytes, uint32_t mips, VkFilter filter, VkSamplerMipmapMode mip_mode, uint32_t array_layers = 1U, VkSamplerAddressMode address = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) -> bool { out.mips = std::max(1U, mips); const auto layers = std::max(1U, array_layers); out.width = w; out.height = h; VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {w, h, 1U}; image_info.mipLevels = out.mips; image_info.arrayLayers = layers; image_info.format = format; image_info.tiling = VK_IMAGE_TILING_OPTIMAL; image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; image_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; image_info.samples = VK_SAMPLE_COUNT_1_BIT; image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (!detail::check(vkCreateImage(device_, &image_info, nullptr, &out.image), "vkCreateImage(terrain)")) return false; VkMemoryRequirements requirements{}; vkGetImageMemoryRequirements(device_, out.image, &requirements); VkMemoryAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &out.memory), "vkAllocateMemory(terrain)")) return false; if (!detail::check(vkBindImageMemory(device_, out.image, out.memory, 0), "vkBindImageMemory")) return false; VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_memory = VK_NULL_HANDLE; if (bytes > 0U) { VkBufferCreateInfo buffer_info{}; buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; buffer_info.size = bytes; buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer(terrain)")) return false; VkMemoryRequirements br{}; vkGetBufferMemoryRequirements(device_, staging, &br); VkMemoryAllocateInfo ba{}; ba.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; ba.allocationSize = br.size; ba.memoryTypeIndex = this->memory_type(br.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (!detail::check(vkAllocateMemory(device_, &ba, nullptr, &staging_memory), "vkAllocateMemory(staging terrain)")) return false; if (!detail::check(vkBindBufferMemory(device_, staging, staging_memory, 0), "vkBindBufferMemory")) return false; void *mapped = nullptr; if (!detail::check(vkMapMemory(device_, staging_memory, 0, bytes, 0, &mapped), "vkMapMemory")) return false; std::memcpy(mapped, data, static_cast(bytes)); vkUnmapMemory(device_, staging_memory); } this->immediate([&](VkCommandBuffer cmd) { for (uint32_t level = 0; level < out.mips; ++level) { this->barrier_mip(cmd, out.image, level, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0U, VK_ACCESS_TRANSFER_WRITE_BIT, layers); } VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, layers}; region.imageExtent = {w, h, 1U}; vkCmdCopyBufferToImage(cmd, staging, out.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, ®ion); int32_t mw = static_cast(w); int32_t mh = static_cast(h); for (uint32_t level = 1; level < out.mips; ++level) { this->barrier_mip(cmd, out.image, level - 1U, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, layers); const auto nw = std::max(1, mw / 2); const auto nh = std::max(1, mh / 2); for (uint32_t layer = 0; layer < layers; ++layer) { VkImageBlit blit{}; blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level - 1U, layer, 1U}; blit.srcOffsets[1] = {mw, mh, 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1U}; blit.dstOffsets[1] = {nw, nh, 1}; vkCmdBlitImage(cmd, out.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, out.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, &blit, VK_FILTER_LINEAR); } mw = nw; mh = nh; } for (uint32_t level = 0; level < out.mips; ++level) { const auto from = level + 1U < out.mips ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; const auto src_access = level + 1U < out.mips ? VK_ACCESS_TRANSFER_READ_BIT : VK_ACCESS_TRANSFER_WRITE_BIT; this->barrier_mip(cmd, out.image, level, from, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, src_access, VK_ACCESS_SHADER_READ_BIT, layers); } }); if (staging != VK_NULL_HANDLE) vkDestroyBuffer(device_, staging, nullptr); if (staging_memory != VK_NULL_HANDLE) vkFreeMemory(device_, staging_memory, nullptr); VkImageViewCreateInfo view_info{}; view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view_info.image = out.image; view_info.viewType = layers > 1U ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; view_info.format = format; view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, out.mips, 0U, layers}; if (!detail::check(vkCreateImageView(device_, &view_info, nullptr, &out.view), "vkCreateImageView(terrain)")) return false; VkSamplerCreateInfo sampler_info{}; sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; sampler_info.magFilter = filter; sampler_info.minFilter = filter; sampler_info.mipmapMode = mip_mode; sampler_info.addressModeU = address; sampler_info.addressModeV = address; sampler_info.addressModeW = address; sampler_info.maxLod = static_cast(out.mips); return detail::check(vkCreateSampler(device_, &sampler_info, nullptr, &out.sampler), "vkCreateSampler(terrain)"); } [[nodiscard]] auto create_terrain_images(const ra3::terrain::gpu_terrain &terrain) -> bool { const auto height_bytes = static_cast(terrain.width) * terrain.height * 2U; const auto cell_bytes = static_cast(terrain.width) * terrain.height * 8U; const auto layer_bytes = static_cast(terrain.layer_count) * terrain.layer_size * terrain.layer_size * 4U; // No mip chain: the array mips were the source of the corrupted // patches, and the raymarcher samples close to 1:1 anyway. const auto mips = 1U; if (!this->make_gpu_image(height_texture_, terrain.width, terrain.height, VK_FORMAT_R16_UNORM, terrain.heights.data(), height_bytes, 1U, VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST)) return false; if (!this->make_gpu_image(tile_texture_, terrain.width, terrain.height, VK_FORMAT_R16G16B16A16_UNORM, terrain.cell_data.data(), cell_bytes, 1U, VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST)) return false; return this->make_gpu_image(atlas_texture_, terrain.layer_size, terrain.layer_size, VK_FORMAT_B8G8R8A8_UNORM, terrain.layers.data(), layer_bytes, mips, VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR, terrain.layer_count, VK_SAMPLER_ADDRESS_MODE_REPEAT); } [[nodiscard]] auto create_terrain_pipeline() -> bool { VkDescriptorSetLayoutBinding bindings[3]{}; for (uint32_t i = 0; i < 3U; ++i) { bindings[i].binding = i; bindings[i].descriptorCount = 1U; bindings[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } VkDescriptorSetLayoutCreateInfo layout_info{}; layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layout_info.bindingCount = 3U; layout_info.pBindings = bindings; if (!detail::check(vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &terrain_layout_), "vkCreateDescriptorSetLayout(terrain)")) return false; VkPushConstantRange push_range{}; push_range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; push_range.size = sizeof(float) * 20U; VkPipelineLayoutCreateInfo pipeline_layout_info{}; pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipeline_layout_info.setLayoutCount = 1U; pipeline_layout_info.pSetLayouts = &terrain_layout_; pipeline_layout_info.pushConstantRangeCount = 1U; pipeline_layout_info.pPushConstantRanges = &push_range; if (!detail::check(vkCreatePipelineLayout(device_, &pipeline_layout_info, nullptr, &terrain_pipeline_layout_), "vkCreatePipelineLayout(terrain)")) return false; VkShaderModule vertex = VK_NULL_HANDLE; VkShaderModule fragment = VK_NULL_HANDLE; if (!this->make_shader_module(ra3_shaders::terrain_vert_spv, ra3_shaders::terrain_vert_spv_words, vertex)) return false; if (!this->make_shader_module(ra3_shaders::terrain_frag_spv, ra3_shaders::terrain_frag_spv_words, fragment)) return false; VkPipelineShaderStageCreateInfo stages[2]{}; stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex; stages[0].pName = "main"; stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment; stages[1].pName = "main"; VkPipelineVertexInputStateCreateInfo vertex_input{}; vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; VkPipelineInputAssemblyStateCreateInfo assembly{}; assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewport{}; viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport.viewportCount = 1U; viewport.scissorCount = 1U; VkPipelineRasterizationStateCreateInfo raster{}; raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; raster.polygonMode = VK_POLYGON_MODE_FILL; raster.cullMode = VK_CULL_MODE_NONE; raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; raster.lineWidth = 1.0F; VkPipelineMultisampleStateCreateInfo multisample{}; multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo depth_stencil{}; depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depth_stencil.depthTestEnable = VK_TRUE; depth_stencil.depthWriteEnable = VK_TRUE; depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; VkPipelineColorBlendAttachmentState blend_attachment{}; blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; VkPipelineColorBlendStateCreateInfo blend{}; blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; blend.attachmentCount = 1U; blend.pAttachments = &blend_attachment; const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = 2U; dynamic.pDynamicStates = dynamic_states; VkGraphicsPipelineCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipeline_info.stageCount = 2U; pipeline_info.pStages = stages; pipeline_info.pVertexInputState = &vertex_input; pipeline_info.pInputAssemblyState = &assembly; pipeline_info.pViewportState = &viewport; pipeline_info.pRasterizationState = &raster; pipeline_info.pMultisampleState = &multisample; pipeline_info.pDepthStencilState = &depth_stencil; pipeline_info.pColorBlendState = &blend; pipeline_info.pDynamicState = &dynamic; pipeline_info.layout = terrain_pipeline_layout_; pipeline_info.renderPass = render_pass_; pipeline_info.subpass = 0U; const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &terrain_pipeline_), "vkCreateGraphicsPipelines(terrain)"); vkDestroyShaderModule(device_, vertex, nullptr); vkDestroyShaderModule(device_, fragment, nullptr); return created; } [[nodiscard]] auto create_terrain_descriptors() -> bool { VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3U}; VkDescriptorPoolCreateInfo pool_info{}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.maxSets = 1U; pool_info.poolSizeCount = 1U; pool_info.pPoolSizes = &pool_size; if (!detail::check(vkCreateDescriptorPool(device_, &pool_info, nullptr, &terrain_pool_), "vkCreateDescriptorPool(terrain)")) return false; VkDescriptorSetAllocateInfo set_info{}; set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; set_info.descriptorPool = terrain_pool_; set_info.descriptorSetCount = 1U; set_info.pSetLayouts = &terrain_layout_; if (!detail::check(vkAllocateDescriptorSets(device_, &set_info, &terrain_set_), "vkAllocateDescriptorSets(terrain)")) return false; VkDescriptorImageInfo infos[3]{}; const gpu_image *images[3] = {&height_texture_, &tile_texture_, &atlas_texture_}; VkWriteDescriptorSet writes[3]{}; for (uint32_t i = 0; i < 3U; ++i) { infos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; infos[i].imageView = images[i]->view; infos[i].sampler = images[i]->sampler; writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = terrain_set_; writes[i].dstBinding = i; writes[i].descriptorCount = 1U; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[i].pImageInfo = &infos[i]; } vkUpdateDescriptorSets(device_, 3U, writes, 0U, nullptr); return true; } /** Host-visible vertex/index buffer, filled once (the map's geometry is static). */ [[nodiscard]] auto make_host_buffer(const void *data, VkDeviceSize bytes, VkBufferUsageFlags usage, VkBuffer &buffer, VkDeviceMemory &memory) -> bool { VkBufferCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.size = bytes; info.usage = usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (!detail::check(vkCreateBuffer(device_, &info, nullptr, &buffer), "vkCreateBuffer(object)")) return false; VkMemoryRequirements requirements{}; vkGetBufferMemoryRequirements(device_, buffer, &requirements); VkMemoryAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &memory), "vkAllocateMemory(object)")) return false; if (!detail::check(vkBindBufferMemory(device_, buffer, memory, 0), "vkBindBufferMemory(object)")) return false; void *mapped = nullptr; if (!detail::check(vkMapMemory(device_, memory, 0, bytes, 0, &mapped), "vkMapMemory(object)")) return false; std::memcpy(mapped, data, static_cast(bytes)); vkUnmapMemory(device_, memory); return true; } /** Upload the static-map geometry and its texture array. */ [[nodiscard]] auto create_object_buffers(const ra3::terrain::gpu_terrain &terrain) -> bool { const auto &scene = terrain.objects; if (scene.empty()) return true; const auto vertex_bytes = static_cast(scene.vertices.size()) * sizeof(ra3::models::vertex); const auto index_bytes = static_cast(scene.indices.size()) * sizeof(uint32_t); if (!this->make_host_buffer(scene.vertices.data(), vertex_bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, object_vertex_buffer_, object_vertex_memory_)) return false; if (!this->make_host_buffer(scene.indices.data(), index_bytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, object_index_buffer_, object_index_memory_)) return false; object_index_count_ = static_cast(scene.indices.size()); const auto size = std::max(1U, scene.texture_size); const auto layers = std::max(1U, scene.textures.size()); std::vector pixels(static_cast(size) * size * layers, 0xFFFFFFFFU); for (size_t layer = 0; layer < scene.textures.size(); ++layer) { const auto &texture = scene.textures[layer]; if (texture.empty()) continue; // The scene already resized every texture to `texture_size`. for (uint32_t y = 0; y < std::min(size, texture.height()); ++y) { for (uint32_t x = 0; x < std::min(size, texture.width()); ++x) { pixels[(static_cast(layer) * size + y) * size + x] = texture.data()[static_cast(y) * texture.width() + x]; } } } return this->make_gpu_image(object_texture_, size, size, VK_FORMAT_B8G8R8A8_UNORM, pixels.data(), static_cast(pixels.size()) * 4U, this->mip_count(size), VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR, static_cast(layers), VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); } /** Full mip chain length for a square texture. */ [[nodiscard]] static auto mip_count(uint32_t size) -> uint32_t { uint32_t mips = 1U; while (size > 1U) { size >>= 1U; ++mips; } return mips; } [[nodiscard]] auto create_object_pipeline() -> bool { VkPushConstantRange push_range{}; push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; push_range.size = sizeof(float) * 20U; VkPipelineLayoutCreateInfo layout_info{}; layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; layout_info.setLayoutCount = 1U; layout_info.pSetLayouts = &descriptor_layout_; // reused: one combined image sampler layout_info.pushConstantRangeCount = 1U; layout_info.pPushConstantRanges = &push_range; if (!detail::check(vkCreatePipelineLayout(device_, &layout_info, nullptr, &object_pipeline_layout_), "vkCreatePipelineLayout(object)")) return false; VkShaderModule vertex = VK_NULL_HANDLE; VkShaderModule fragment = VK_NULL_HANDLE; if (!this->make_shader_module(ra3_shaders::object_vert_spv, ra3_shaders::object_vert_spv_words, vertex)) return false; if (!this->make_shader_module(ra3_shaders::object_frag_spv, ra3_shaders::object_frag_spv_words, fragment)) return false; VkPipelineShaderStageCreateInfo stages[2]{}; stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex; stages[0].pName = "main"; stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment; stages[1].pName = "main"; VkVertexInputBindingDescription binding{}; binding.binding = 0U; binding.stride = sizeof(ra3::models::vertex); binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; VkVertexInputAttributeDescription attributes[4]{}; attributes[0] = {0U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 0U}; attributes[1] = {1U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 12U}; attributes[2] = {2U, 0U, VK_FORMAT_R32G32_SFLOAT, 24U}; attributes[3] = {3U, 0U, VK_FORMAT_R32_SFLOAT, 32U}; static_assert(sizeof(ra3::models::vertex) == 36U, "object vertex layout changed"); VkPipelineVertexInputStateCreateInfo vertex_input{}; vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; vertex_input.vertexBindingDescriptionCount = 1U; vertex_input.pVertexBindingDescriptions = &binding; vertex_input.vertexAttributeDescriptionCount = 4U; vertex_input.pVertexAttributeDescriptions = attributes; VkPipelineInputAssemblyStateCreateInfo assembly{}; assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewport{}; viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport.viewportCount = 1U; viewport.scissorCount = 1U; VkPipelineRasterizationStateCreateInfo raster{}; raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; raster.polygonMode = VK_POLYGON_MODE_FILL; raster.cullMode = VK_CULL_MODE_NONE; raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; raster.lineWidth = 1.0F; // Ground decals (sidewalks/roads/deck pieces) sit exactly on the // terrain; a small negative depth bias keeps them from z-fighting. raster.depthBiasEnable = VK_TRUE; raster.depthBiasConstantFactor = -1.0F; raster.depthBiasSlopeFactor = -1.0F; VkPipelineMultisampleStateCreateInfo multisample{}; multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo depth_stencil{}; depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depth_stencil.depthTestEnable = VK_TRUE; depth_stencil.depthWriteEnable = VK_TRUE; depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; VkPipelineColorBlendAttachmentState blend_attachment{}; blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; blend_attachment.blendEnable = VK_FALSE; VkPipelineColorBlendStateCreateInfo blend{}; blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; blend.attachmentCount = 1U; blend.pAttachments = &blend_attachment; const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = 2U; dynamic.pDynamicStates = dynamic_states; VkGraphicsPipelineCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipeline_info.stageCount = 2U; pipeline_info.pStages = stages; pipeline_info.pVertexInputState = &vertex_input; pipeline_info.pInputAssemblyState = &assembly; pipeline_info.pViewportState = &viewport; pipeline_info.pRasterizationState = &raster; pipeline_info.pMultisampleState = &multisample; pipeline_info.pDepthStencilState = &depth_stencil; pipeline_info.pColorBlendState = &blend; pipeline_info.pDynamicState = &dynamic; pipeline_info.layout = object_pipeline_layout_; pipeline_info.renderPass = render_pass_; pipeline_info.subpass = 0U; const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &object_pipeline_), "vkCreateGraphicsPipelines(object)"); vkDestroyShaderModule(device_, vertex, nullptr); vkDestroyShaderModule(device_, fragment, nullptr); return created; } [[nodiscard]] auto create_object_descriptors() -> bool { if (object_texture_.view == VK_NULL_HANDLE) return false; VkDescriptorSetAllocateInfo set_info{}; set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; set_info.descriptorPool = descriptor_pool_; set_info.descriptorSetCount = 1U; set_info.pSetLayouts = &descriptor_layout_; if (!detail::check(vkAllocateDescriptorSets(device_, &set_info, &object_set_), "vkAllocateDescriptorSets(object)")) return false; VkDescriptorImageInfo info{}; info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; info.imageView = object_texture_.view; info.sampler = object_texture_.sampler; VkWriteDescriptorSet write{}; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; write.dstSet = object_set_; write.dstBinding = 0U; write.descriptorCount = 1U; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = &info; vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr); return true; } [[nodiscard]] auto draw_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s, float aspect) -> bool { vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX); uint32_t image_index = 0; const auto acquire = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, image_available_[current_frame_], VK_NULL_HANDLE, &image_index); if (acquire == VK_ERROR_OUT_OF_DATE_KHR) return this->recreate_swapchain(); if (acquire != VK_SUCCESS && acquire != VK_SUBOPTIMAL_KHR) return false; vkResetFences(device_, 1U, &in_flight_[current_frame_]); const auto cmd = command_buffers_[current_frame_]; vkResetCommandBuffer(cmd, 0U); VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkBeginCommandBuffer(cmd, &begin); VkClearValue clear{}; clear.color = {{0.45F, 0.55F, 0.70F, 1.0F}}; VkClearValue depth_clear{}; depth_clear.depthStencil = {1.0F, 0U}; const VkClearValue clears[2] = {clear, depth_clear}; VkRenderPassBeginInfo render_pass_begin{}; render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; render_pass_begin.renderPass = render_pass_; render_pass_begin.framebuffer = framebuffers_[image_index]; render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_}; render_pass_begin.clearValueCount = 2U; render_pass_begin.pClearValues = clears; vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE); VkViewport viewport{0.0F, 0.0F, static_cast(swapchain_extent_.width), static_cast(swapchain_extent_.height), 0.0F, 1.0F}; VkRect2D scissor{{0U, 0U}, swapchain_extent_}; vkCmdSetViewport(cmd, 0U, 1U, &viewport); vkCmdSetScissor(cmd, 0U, 1U, &scissor); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrain_pipeline_); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrain_pipeline_layout_, 0U, 1U, &terrain_set_, 0U, nullptr); const float push[20] = {camera.target_x, camera.target_y, camera.yaw, camera.height, camera.pitch, camera.fov, terrain.water_z, terrain.has_water ? 1.0F : 0.0F, 0.45F, 0.35F, 0.82F, 0.38F, static_cast(terrain.width), static_cast(terrain.height), 0.0F, terrain.z_scale, time_s, 0.0F, terrain.cell_span, aspect}; vkCmdPushConstants(cmd, terrain_pipeline_layout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(push), push); vkCmdDraw(cmd, 3U, 1U, 0U, 0U); // Static-map models (buildings/props) depth-test against the terrain. if (objects_ready_ && object_index_count_ > 0U) { const auto z_scale = terrain.z_scale; const auto world_w = static_cast(terrain.width) * 10.0F; const auto world_h = static_cast(terrain.height) * 10.0F; const auto sample = [&](float wx, float wy) -> float { if (wx < 0.0F || wy < 0.0F || wx >= world_w || wy >= world_h) return -1.0e9F; const auto cx = std::min(terrain.width - 1U, static_cast(wx / 10.0F)); const auto cy = std::min(terrain.height - 1U, static_cast((world_h - wy) / 10.0F)); return static_cast(terrain.heights[static_cast(cy) * terrain.width + cx]) * z_scale; }; float target_z = sample(camera.target_x, camera.target_y); if (target_z < -1.0e8F) target_z = 0.0F; const auto pitch = std::clamp(camera.pitch, 0.15F, 1.45F); const auto fov = std::clamp(camera.fov, 0.3F, 1.4F); const auto cp = std::cos(pitch); const float fwd[3] = {cp * std::sin(camera.yaw), cp * std::cos(camera.yaw), -std::sin(pitch)}; float right[3] = {fwd[1], -fwd[0], 0.0F}; const auto rl = std::sqrt(right[0] * right[0] + right[1] * right[1]); right[0] /= rl; right[1] /= rl; float up[3] = {right[1] * fwd[2], -right[0] * fwd[2], right[0] * fwd[1] - right[1] * fwd[0]}; const auto ul = std::sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]); up[0] /= ul; up[1] /= ul; up[2] /= ul; const auto dist = camera.height / std::sin(pitch); const float cam[3] = {camera.target_x - fwd[0] * dist, camera.target_y - fwd[1] * dist, target_z + camera.height - fwd[2] * dist}; const auto th = std::tan(fov * 0.5F); const float obj_push[20] = {cam[0], cam[1], cam[2], 0.0F, fwd[0], fwd[1], fwd[2], 0.0F, right[0], right[1], right[2], th, up[0], up[1], up[2], th * aspect, 0.45F, 0.35F, 0.82F, 0.38F}; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object_pipeline_); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object_pipeline_layout_, 0U, 1U, &object_set_, 0U, nullptr); vkCmdPushConstants(cmd, object_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(obj_push), obj_push); const VkDeviceSize offset = 0U; vkCmdBindVertexBuffers(cmd, 0U, 1U, &object_vertex_buffer_, &offset); vkCmdBindIndexBuffer(cmd, object_index_buffer_, 0U, VK_INDEX_TYPE_UINT32); vkCmdDrawIndexed(cmd, object_index_count_, 1U, 0U, 0, 0U); } // Overlays (top-left FPS label, bottom-right minimap) use the image // pipeline with alpha blending, drawn into the same render pass. const auto draw_overlay = [&](const gpu_image &overlay, VkDescriptorSet set, uint32_t w, uint32_t h, float x, float y) { if (set == VK_NULL_HANDLE || overlay.image == VK_NULL_HANDLE || w == 0U || h == 0U) return; vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout_, 0U, 1U, &set, 0U, nullptr); const float rect[4] = {x / static_cast(swapchain_extent_.width), y / static_cast(swapchain_extent_.height), static_cast(w) / static_cast(swapchain_extent_.width), static_cast(h) / static_cast(swapchain_extent_.height)}; vkCmdPushConstants(cmd, pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0U, sizeof(rect), rect); vkCmdDraw(cmd, 3U, 1U, 0U, 0U); }; const float margin = 12.0F; draw_overlay(overlay_label_, overlay_label_set_, overlay_label_w_, overlay_label_h_, margin, margin); // Minimap lives in the top-right corner. draw_overlay(overlay_minimap_, overlay_minimap_set_, overlay_minimap_w_, overlay_minimap_h_, static_cast(swapchain_extent_.width) - static_cast(overlay_minimap_w_) - margin, margin); vkCmdEndRenderPass(cmd); vkEndCommandBuffer(cmd); const VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit.waitSemaphoreCount = 1U; submit.pWaitSemaphores = &image_available_[current_frame_]; submit.pWaitDstStageMask = &wait_stage; submit.commandBufferCount = 1U; submit.pCommandBuffers = &cmd; submit.signalSemaphoreCount = 1U; submit.pSignalSemaphores = &render_finished_[current_frame_]; if (!detail::check(vkQueueSubmit(queue_, 1U, &submit, in_flight_[current_frame_]), "vkQueueSubmit(terrain)")) return false; VkPresentInfoKHR present{}; present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.waitSemaphoreCount = 1U; present.pWaitSemaphores = &render_finished_[current_frame_]; present.swapchainCount = 1U; present.pSwapchains = &swapchain_; present.pImageIndices = &image_index; const auto result = vkQueuePresentKHR(queue_, &present); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { if (!this->recreate_swapchain()) return false; } else if (result != VK_SUCCESS) { return false; } current_frame_ = (current_frame_ + 1U) % frames_in_flight; return true; } auto destroy_terrain() -> void { for (gpu_image *image: {&height_texture_, &tile_texture_, &atlas_texture_, &overlay_label_, &overlay_minimap_}) { if (image->sampler != VK_NULL_HANDLE) vkDestroySampler(device_, image->sampler, nullptr); if (image->view != VK_NULL_HANDLE) vkDestroyImageView(device_, image->view, nullptr); if (image->image != VK_NULL_HANDLE) vkDestroyImage(device_, image->image, nullptr); if (image->memory != VK_NULL_HANDLE) vkFreeMemory(device_, image->memory, nullptr); *image = {}; } if (terrain_pool_ != VK_NULL_HANDLE) vkDestroyDescriptorPool(device_, terrain_pool_, nullptr); if (terrain_pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, terrain_pipeline_, nullptr); if (terrain_pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, terrain_pipeline_layout_, nullptr); if (terrain_layout_ != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(device_, terrain_layout_, nullptr); terrain_pool_ = VK_NULL_HANDLE; terrain_pipeline_ = VK_NULL_HANDLE; terrain_pipeline_layout_ = VK_NULL_HANDLE; terrain_layout_ = VK_NULL_HANDLE; terrain_set_ = VK_NULL_HANDLE; overlay_label_set_ = VK_NULL_HANDLE; overlay_minimap_set_ = VK_NULL_HANDLE; if (object_vertex_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, object_vertex_buffer_, nullptr); if (object_index_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, object_index_buffer_, nullptr); if (object_vertex_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, object_vertex_memory_, nullptr); if (object_index_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, object_index_memory_, nullptr); if (object_texture_.sampler != VK_NULL_HANDLE) vkDestroySampler(device_, object_texture_.sampler, nullptr); if (object_texture_.view != VK_NULL_HANDLE) vkDestroyImageView(device_, object_texture_.view, nullptr); if (object_texture_.image != VK_NULL_HANDLE) vkDestroyImage(device_, object_texture_.image, nullptr); if (object_texture_.memory != VK_NULL_HANDLE) vkFreeMemory(device_, object_texture_.memory, nullptr); if (object_pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, object_pipeline_, nullptr); if (object_pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, object_pipeline_layout_, nullptr); object_vertex_buffer_ = VK_NULL_HANDLE; object_index_buffer_ = VK_NULL_HANDLE; object_vertex_memory_ = VK_NULL_HANDLE; object_index_memory_ = VK_NULL_HANDLE; object_texture_ = {}; object_pipeline_ = VK_NULL_HANDLE; object_pipeline_layout_ = VK_NULL_HANDLE; object_set_ = VK_NULL_HANDLE; object_index_count_ = 0U; objects_ready_ = false; } // ---- device selection ------------------------------------------------ [[nodiscard]] auto pick_device() -> bool { uint32_t count = 0; vkEnumeratePhysicalDevices(instance_, &count, nullptr); if (count == 0U) return false; std::vector devices(count); vkEnumeratePhysicalDevices(instance_, &count, devices.data()); for (const auto device: devices) { uint32_t family_count = 0; vkGetPhysicalDeviceQueueFamilyProperties(device, &family_count, nullptr); std::vector families(family_count); vkGetPhysicalDeviceQueueFamilyProperties(device, &family_count, families.data()); bool has_graphics = false; bool has_present = false; for (uint32_t i = 0; i < family_count; ++i) { if ((families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U) has_graphics = true; VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface_, &present); if (present == VK_TRUE) has_present = true; } if (has_graphics && has_present) { physical_ = device; return true; } } return false; } [[nodiscard]] auto create_device() -> bool { uint32_t family_count = 0; vkGetPhysicalDeviceQueueFamilyProperties(physical_, &family_count, nullptr); std::vector families(family_count); vkGetPhysicalDeviceQueueFamilyProperties(physical_, &family_count, families.data()); queue_family_ = UINT32_MAX; for (uint32_t i = 0; i < family_count; ++i) { if ((families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0U) continue; VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(physical_, i, surface_, &present); if (present == VK_TRUE) { queue_family_ = i; break; } } if (queue_family_ == UINT32_MAX) return false; const float priority = 1.0F; VkDeviceQueueCreateInfo queue_info{}; queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queue_info.queueFamilyIndex = queue_family_; queue_info.queueCount = 1U; queue_info.pQueuePriorities = &priority; const char *extensions[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; VkDeviceCreateInfo device_info{}; device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; device_info.queueCreateInfoCount = 1U; device_info.pQueueCreateInfos = &queue_info; device_info.enabledExtensionCount = 1U; device_info.ppEnabledExtensionNames = extensions; if (!detail::check(vkCreateDevice(physical_, &device_info, nullptr, &device_), "vkCreateDevice")) return false; volkLoadDevice(device_); vkGetDeviceQueue(device_, queue_family_, 0U, &queue_); return true; } [[nodiscard]] auto memory_type(uint32_t bits, VkMemoryPropertyFlags properties) const -> uint32_t { VkPhysicalDeviceMemoryProperties memory{}; vkGetPhysicalDeviceMemoryProperties(physical_, &memory); for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) { if ((bits & (1U << i)) != 0U && (memory.memoryTypes[i].propertyFlags & properties) == properties) return i; } return 0U; } // ---- swapchain ------------------------------------------------------- [[nodiscard]] auto create_swapchain() -> bool { VkSurfaceCapabilitiesKHR caps{}; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_, surface_, &caps); uint32_t format_count = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_, &format_count, nullptr); std::vector formats(format_count); vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_, &format_count, formats.data()); VkSurfaceFormatKHR chosen_format{}; if (formats.empty()) { chosen_format = {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; } else { chosen_format = formats[0]; } for (const auto &candidate: formats) { if (candidate.format == VK_FORMAT_B8G8R8A8_UNORM && candidate.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen_format = candidate; } VkExtent2D extent = caps.currentExtent; if (extent.width == UINT32_MAX) { int width = 0; int height = 0; SDL_GetWindowSizeInPixels(window_, &width, &height); extent.width = std::clamp(static_cast(width), caps.minImageExtent.width, caps.maxImageExtent.width); extent.height = std::clamp(static_cast(height), caps.minImageExtent.height, caps.maxImageExtent.height); } uint32_t image_count = caps.minImageCount + 1U; if (caps.maxImageCount > 0U && image_count > caps.maxImageCount) image_count = caps.maxImageCount; VkSwapchainCreateInfoKHR swapchain_info{}; swapchain_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; swapchain_info.surface = surface_; swapchain_info.minImageCount = image_count; swapchain_info.imageFormat = chosen_format.format; swapchain_info.imageColorSpace = chosen_format.colorSpace; swapchain_info.imageExtent = extent; swapchain_info.imageArrayLayers = 1U; swapchain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; swapchain_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchain_info.preTransform = caps.currentTransform; swapchain_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; swapchain_info.presentMode = this->fps_limit_ == 0 ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR; swapchain_info.clipped = VK_TRUE; if (!detail::check(vkCreateSwapchainKHR(device_, &swapchain_info, nullptr, &swapchain_), "vkCreateSwapchainKHR")) return false; uint32_t actual = 0; vkGetSwapchainImagesKHR(device_, swapchain_, &actual, nullptr); swapchain_images_.resize(actual); vkGetSwapchainImagesKHR(device_, swapchain_, &actual, swapchain_images_.data()); swapchain_format_ = chosen_format.format; swapchain_extent_ = extent; swapchain_views_.resize(actual); for (uint32_t i = 0; i < actual; ++i) { VkImageViewCreateInfo view_info{}; view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view_info.image = swapchain_images_[i]; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; view_info.format = swapchain_format_; view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 1U, 0U, 1U}; if (!detail::check(vkCreateImageView(device_, &view_info, nullptr, &swapchain_views_[i]), "vkCreateImageView(swapchain)")) return false; } framebuffers_.resize(actual); return true; } /** Create one color+depth framebuffer per swapchain image (needs `render_pass_`). */ [[nodiscard]] auto create_framebuffers() -> bool { this->create_depth_resources(); for (size_t i = 0; i < framebuffers_.size(); ++i) { VkImageView attachments[] = {swapchain_views_[i], depth_views_[i]}; VkFramebufferCreateInfo framebuffer_info{}; framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; framebuffer_info.renderPass = render_pass_; framebuffer_info.attachmentCount = 2U; framebuffer_info.pAttachments = attachments; framebuffer_info.width = swapchain_extent_.width; framebuffer_info.height = swapchain_extent_.height; framebuffer_info.layers = 1U; if (!detail::check(vkCreateFramebuffer(device_, &framebuffer_info, nullptr, &framebuffers_[i]), "vkCreateFramebuffer")) return false; } return true; } /** Allocate a depth image + view per swapchain image (static-map models depth-test against the terrain). */ auto create_depth_resources() -> void { this->destroy_depth_resources(); depth_images_.resize(swapchain_images_.size()); depth_memories_.resize(swapchain_images_.size()); depth_views_.resize(swapchain_images_.size()); for (size_t i = 0; i < swapchain_images_.size(); ++i) { VkImageCreateInfo image_info{}; image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.extent = {swapchain_extent_.width, swapchain_extent_.height, 1U}; image_info.mipLevels = 1U; image_info.arrayLayers = 1U; image_info.format = VK_FORMAT_D32_SFLOAT; image_info.tiling = VK_IMAGE_TILING_OPTIMAL; image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; image_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; image_info.samples = VK_SAMPLE_COUNT_1_BIT; image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateImage(device_, &image_info, nullptr, &depth_images_[i]) != VK_SUCCESS) return; VkMemoryRequirements requirements{}; vkGetImageMemoryRequirements(device_, depth_images_[i], &requirements); VkMemoryAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); if (vkAllocateMemory(device_, &allocate, nullptr, &depth_memories_[i]) != VK_SUCCESS) return; vkBindImageMemory(device_, depth_images_[i], depth_memories_[i], 0); VkImageViewCreateInfo view_info{}; view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view_info.image = depth_images_[i]; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; view_info.format = VK_FORMAT_D32_SFLOAT; view_info.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0U, 1U, 0U, 1U}; if (vkCreateImageView(device_, &view_info, nullptr, &depth_views_[i]) != VK_SUCCESS) return; } } auto destroy_depth_resources() -> void { if (device_ == VK_NULL_HANDLE) return; for (const auto view: depth_views_) vkDestroyImageView(device_, view, nullptr); for (const auto image: depth_images_) vkDestroyImage(device_, image, nullptr); for (const auto memory: depth_memories_) vkFreeMemory(device_, memory, nullptr); depth_views_.clear(); depth_images_.clear(); depth_memories_.clear(); } auto cleanup_swapchain() -> void { for (const auto framebuffer: framebuffers_) vkDestroyFramebuffer(device_, framebuffer, nullptr); this->destroy_depth_resources(); for (const auto view: swapchain_views_) vkDestroyImageView(device_, view, nullptr); framebuffers_.clear(); swapchain_views_.clear(); swapchain_images_.clear(); if (swapchain_ != VK_NULL_HANDLE) vkDestroySwapchainKHR(device_, swapchain_, nullptr); swapchain_ = VK_NULL_HANDLE; } [[nodiscard]] auto recreate_swapchain() -> bool { int width = 0; int height = 0; SDL_GetWindowSizeInPixels(window_, &width, &height); if (width == 0 || height == 0) return false; vkDeviceWaitIdle(device_); this->cleanup_swapchain(); return this->create_swapchain() && this->create_framebuffers(); } // ---- pipeline -------------------------------------------------------- [[nodiscard]] auto create_render_pass() -> bool { VkAttachmentDescription color{}; color.format = swapchain_format_; color.samples = VK_SAMPLE_COUNT_1_BIT; color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; VkAttachmentReference color_ref{0U, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; VkAttachmentDescription depth{}; depth.format = VK_FORMAT_D32_SFLOAT; depth.samples = VK_SAMPLE_COUNT_1_BIT; depth.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; depth.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depth.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depth.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; depth.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkAttachmentReference depth_ref{1U, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1U; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref; VkSubpassDependency dependency{}; dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0U; dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; const VkAttachmentDescription attachments[] = {color, depth}; VkRenderPassCreateInfo render_pass_info{}; render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; render_pass_info.attachmentCount = 2U; render_pass_info.pAttachments = attachments; render_pass_info.subpassCount = 1U; render_pass_info.pSubpasses = &subpass; render_pass_info.dependencyCount = 1U; render_pass_info.pDependencies = &dependency; return detail::check(vkCreateRenderPass(device_, &render_pass_info, nullptr, &render_pass_), "vkCreateRenderPass"); } [[nodiscard]] auto make_shader_module(const uint32_t *code, std::size_t words, VkShaderModule &out) -> bool { VkShaderModuleCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; info.codeSize = words * sizeof(uint32_t); info.pCode = code; return detail::check(vkCreateShaderModule(device_, &info, nullptr, &out), "vkCreateShaderModule"); } [[nodiscard]] auto create_pipeline() -> bool { VkDescriptorSetLayoutBinding binding{}; binding.binding = 0U; binding.descriptorCount = 1U; binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayoutCreateInfo layout_info{}; layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layout_info.bindingCount = 1U; layout_info.pBindings = &binding; if (!detail::check(vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &descriptor_layout_), "vkCreateDescriptorSetLayout")) return false; VkPushConstantRange push_range{}; push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; push_range.size = sizeof(float) * 4U; VkPipelineLayoutCreateInfo pipeline_layout_info{}; pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipeline_layout_info.setLayoutCount = 1U; pipeline_layout_info.pSetLayouts = &descriptor_layout_; pipeline_layout_info.pushConstantRangeCount = 1U; pipeline_layout_info.pPushConstantRanges = &push_range; if (!detail::check(vkCreatePipelineLayout(device_, &pipeline_layout_info, nullptr, &pipeline_layout_), "vkCreatePipelineLayout")) return false; VkShaderModule vertex = VK_NULL_HANDLE; VkShaderModule fragment = VK_NULL_HANDLE; if (!this->make_shader_module(ra3_shaders::scene_vert_spv, ra3_shaders::scene_vert_spv_words, vertex)) return false; if (!this->make_shader_module(ra3_shaders::scene_frag_spv, ra3_shaders::scene_frag_spv_words, fragment)) return false; VkPipelineShaderStageCreateInfo stages[2]{}; stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex; stages[0].pName = "main"; stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment; stages[1].pName = "main"; VkPipelineVertexInputStateCreateInfo vertex_input{}; vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; VkPipelineInputAssemblyStateCreateInfo assembly{}; assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewport{}; viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport.viewportCount = 1U; viewport.scissorCount = 1U; VkPipelineRasterizationStateCreateInfo raster{}; raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; raster.polygonMode = VK_POLYGON_MODE_FILL; raster.cullMode = VK_CULL_MODE_NONE; raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; raster.lineWidth = 1.0F; VkPipelineMultisampleStateCreateInfo multisample{}; multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineColorBlendAttachmentState blend_attachment{}; blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; blend_attachment.blendEnable = VK_TRUE; blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend_attachment.colorBlendOp = VK_BLEND_OP_ADD; blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD; VkPipelineColorBlendStateCreateInfo blend{}; blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; blend.attachmentCount = 1U; blend.pAttachments = &blend_attachment; const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = 2U; dynamic.pDynamicStates = dynamic_states; VkGraphicsPipelineCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipeline_info.stageCount = 2U; pipeline_info.pStages = stages; pipeline_info.pVertexInputState = &vertex_input; pipeline_info.pInputAssemblyState = &assembly; pipeline_info.pViewportState = &viewport; pipeline_info.pRasterizationState = &raster; pipeline_info.pMultisampleState = &multisample; pipeline_info.pColorBlendState = &blend; pipeline_info.pDynamicState = &dynamic; pipeline_info.layout = pipeline_layout_; pipeline_info.renderPass = render_pass_; pipeline_info.subpass = 0U; const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &pipeline_), "vkCreateGraphicsPipelines"); vkDestroyShaderModule(device_, vertex, nullptr); vkDestroyShaderModule(device_, fragment, nullptr); return created; } // ---- commands, sync, descriptor pool --------------------------------- [[nodiscard]] auto create_commands() -> bool { VkCommandPoolCreateInfo pool_info{}; pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool_info.queueFamilyIndex = queue_family_; if (!detail::check(vkCreateCommandPool(device_, &pool_info, nullptr, &command_pool_), "vkCreateCommandPool")) return false; VkCommandBufferAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocate.commandPool = command_pool_; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = frames_in_flight; command_buffers_.resize(frames_in_flight); if (!detail::check(vkAllocateCommandBuffers(device_, &allocate, command_buffers_.data()), "vkAllocateCommandBuffers")) return false; VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8U}; VkDescriptorPoolCreateInfo descriptor_pool_info{}; descriptor_pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; descriptor_pool_info.maxSets = 8U; descriptor_pool_info.poolSizeCount = 1U; descriptor_pool_info.pPoolSizes = &pool_size; if (!detail::check(vkCreateDescriptorPool(device_, &descriptor_pool_info, nullptr, &descriptor_pool_), "vkCreateDescriptorPool")) return false; VkDescriptorSetAllocateInfo set_info{}; set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; set_info.descriptorPool = descriptor_pool_; set_info.descriptorSetCount = 1U; set_info.pSetLayouts = &descriptor_layout_; return detail::check(vkAllocateDescriptorSets(device_, &set_info, &descriptor_set_), "vkAllocateDescriptorSets"); } [[nodiscard]] auto create_sync() -> bool { VkSemaphoreCreateInfo semaphore_info{}; semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; VkFenceCreateInfo fence_info{}; fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT; image_available_.resize(frames_in_flight); render_finished_.resize(frames_in_flight); in_flight_.resize(frames_in_flight); for (uint32_t i = 0; i < frames_in_flight; ++i) { if (!detail::check(vkCreateSemaphore(device_, &semaphore_info, nullptr, &image_available_[i]), "vkCreateSemaphore")) return false; if (!detail::check(vkCreateSemaphore(device_, &semaphore_info, nullptr, &render_finished_[i]), "vkCreateSemaphore")) return false; if (!detail::check(vkCreateFence(device_, &fence_info, nullptr, &in_flight_[i]), "vkCreateFence")) return false; } return true; } // ---- helpers --------------------------------------------------------- /** Run `record` on a one-shot command buffer and wait for it. */ template auto immediate(Fn &&record) -> void { VkCommandBufferAllocateInfo allocate{}; allocate.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocate.commandPool = command_pool_; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1U; VkCommandBuffer cmd = VK_NULL_HANDLE; vkAllocateCommandBuffers(device_, &allocate, &cmd); VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmd, &begin); record(cmd); vkEndCommandBuffer(cmd); VkSubmitInfo submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit.commandBufferCount = 1U; submit.pCommandBuffers = &cmd; vkQueueSubmit(queue_, 1U, &submit, VK_NULL_HANDLE); vkQueueWaitIdle(queue_); vkFreeCommandBuffers(device_, command_pool_, 1U, &cmd); } static auto transition(VkCommandBuffer cmd, VkImage image, VkImageLayout from, VkImageLayout to) -> void { VkImageMemoryBarrier barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = from; barrier.newLayout = to; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 1U, 0U, 1U}; barrier.srcAccessMask = (to == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) ? 0U : VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = (to == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) ? VK_ACCESS_TRANSFER_WRITE_BIT : VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0U, 0U, nullptr, 0U, nullptr, 1U, &barrier); } /** Record, submit and present one frame. Returns false when the loop should stop. */ [[nodiscard]] auto draw(const ra3::render::view_rect &rect, float window_w, float window_h) -> bool { vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX); uint32_t image_index = 0; const auto acquire = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, image_available_[current_frame_], VK_NULL_HANDLE, &image_index); if (acquire == VK_ERROR_OUT_OF_DATE_KHR) return this->recreate_swapchain(); if (acquire != VK_SUCCESS && acquire != VK_SUBOPTIMAL_KHR) return false; vkResetFences(device_, 1U, &in_flight_[current_frame_]); const auto cmd = command_buffers_[current_frame_]; vkResetCommandBuffer(cmd, 0U); VkCommandBufferBeginInfo begin{}; begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; vkBeginCommandBuffer(cmd, &begin); VkClearValue clear{}; clear.color = {{0.05F, 0.06F, 0.08F, 1.0F}}; VkClearValue depth_clear{}; depth_clear.depthStencil = {1.0F, 0U}; const VkClearValue clears[2] = {clear, depth_clear}; VkRenderPassBeginInfo render_pass_begin{}; render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; render_pass_begin.renderPass = render_pass_; render_pass_begin.framebuffer = framebuffers_[image_index]; render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_}; render_pass_begin.clearValueCount = 2U; render_pass_begin.pClearValues = clears; vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE); VkViewport viewport{0.0F, 0.0F, static_cast(swapchain_extent_.width), static_cast(swapchain_extent_.height), 0.0F, 1.0F}; VkRect2D scissor{{0U, 0U}, swapchain_extent_}; vkCmdSetViewport(cmd, 0U, 1U, &viewport); vkCmdSetScissor(cmd, 0U, 1U, &scissor); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout_, 0U, 1U, &descriptor_set_, 0U, nullptr); // The vertex shader works in window-normalized coordinates (0..1), so // convert the pixel rect the camera produced. const float push[4] = {rect.x / window_w, rect.y / window_h, rect.w / window_w, rect.h / window_h}; vkCmdPushConstants(cmd, pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0U, sizeof(push), push); vkCmdDraw(cmd, 3U, 1U, 0U, 0U); vkCmdEndRenderPass(cmd); vkEndCommandBuffer(cmd); const VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo submit{}; submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit.waitSemaphoreCount = 1U; submit.pWaitSemaphores = &image_available_[current_frame_]; submit.pWaitDstStageMask = &wait_stage; submit.commandBufferCount = 1U; submit.pCommandBuffers = &cmd; submit.signalSemaphoreCount = 1U; submit.pSignalSemaphores = &render_finished_[current_frame_]; if (!detail::check(vkQueueSubmit(queue_, 1U, &submit, in_flight_[current_frame_]), "vkQueueSubmit")) return false; VkPresentInfoKHR present{}; present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.waitSemaphoreCount = 1U; present.pWaitSemaphores = &render_finished_[current_frame_]; present.swapchainCount = 1U; present.pSwapchains = &swapchain_; present.pImageIndices = &image_index; const auto result = vkQueuePresentKHR(queue_, &present); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { if (!this->recreate_swapchain()) return false; } else if (result != VK_SUCCESS) { return false; } current_frame_ = (current_frame_ + 1U) % frames_in_flight; return true; } auto destroy_texture() -> void { if (device_ == VK_NULL_HANDLE) { sampler_ = VK_NULL_HANDLE; texture_view_ = VK_NULL_HANDLE; texture_ = VK_NULL_HANDLE; texture_memory_ = VK_NULL_HANDLE; texture_width_ = 0; texture_height_ = 0; return; } if (sampler_ != VK_NULL_HANDLE) vkDestroySampler(device_, sampler_, nullptr); if (texture_view_ != VK_NULL_HANDLE) vkDestroyImageView(device_, texture_view_, nullptr); if (texture_ != VK_NULL_HANDLE) vkDestroyImage(device_, texture_, nullptr); if (texture_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, texture_memory_, nullptr); sampler_ = VK_NULL_HANDLE; texture_view_ = VK_NULL_HANDLE; texture_ = VK_NULL_HANDLE; texture_memory_ = VK_NULL_HANDLE; texture_width_ = 0; texture_height_ = 0; } auto cleanup() -> void { if (device_ != VK_NULL_HANDLE) { vkDeviceWaitIdle(device_); this->destroy_terrain(); terrain_ready_ = false; this->destroy_texture(); for (const auto semaphore: image_available_) vkDestroySemaphore(device_, semaphore, nullptr); for (const auto semaphore: render_finished_) vkDestroySemaphore(device_, semaphore, nullptr); for (const auto fence: in_flight_) vkDestroyFence(device_, fence, nullptr); image_available_.clear(); render_finished_.clear(); in_flight_.clear(); if (descriptor_pool_ != VK_NULL_HANDLE) vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr); if (pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, pipeline_, nullptr); if (pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr); if (descriptor_layout_ != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(device_, descriptor_layout_, nullptr); if (command_pool_ != VK_NULL_HANDLE) vkDestroyCommandPool(device_, command_pool_, nullptr); if (render_pass_ != VK_NULL_HANDLE) vkDestroyRenderPass(device_, render_pass_, nullptr); this->cleanup_swapchain(); vkDestroyDevice(device_, nullptr); } if (surface_ != VK_NULL_HANDLE) vkDestroySurfaceKHR(instance_, surface_, nullptr); if (instance_ != VK_NULL_HANDLE) vkDestroyInstance(instance_, nullptr); if (window_ != nullptr) SDL_DestroyWindow(window_); SDL_Quit(); device_ = VK_NULL_HANDLE; instance_ = VK_NULL_HANDLE; surface_ = VK_NULL_HANDLE; window_ = nullptr; render_pass_ = VK_NULL_HANDLE; command_pool_ = VK_NULL_HANDLE; descriptor_layout_ = VK_NULL_HANDLE; pipeline_layout_ = VK_NULL_HANDLE; pipeline_ = VK_NULL_HANDLE; descriptor_pool_ = VK_NULL_HANDLE; } SDL_Window *window_ = nullptr; VkInstance instance_ = VK_NULL_HANDLE; VkSurfaceKHR surface_ = VK_NULL_HANDLE; VkPhysicalDevice physical_ = VK_NULL_HANDLE; VkDevice device_ = VK_NULL_HANDLE; VkQueue queue_ = VK_NULL_HANDLE; uint32_t queue_family_ = UINT32_MAX; VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED; VkExtent2D swapchain_extent_{}; std::vector swapchain_images_; std::vector swapchain_views_; std::vector framebuffers_; std::vector depth_images_; std::vector depth_memories_; std::vector depth_views_; VkRenderPass render_pass_ = VK_NULL_HANDLE; VkDescriptorSetLayout descriptor_layout_ = VK_NULL_HANDLE; VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE; VkPipeline pipeline_ = VK_NULL_HANDLE; VkCommandPool command_pool_ = VK_NULL_HANDLE; std::vector command_buffers_; VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE; VkDescriptorSet descriptor_set_ = VK_NULL_HANDLE; VkImage texture_ = VK_NULL_HANDLE; VkDeviceMemory texture_memory_ = VK_NULL_HANDLE; VkImageView texture_view_ = VK_NULL_HANDLE; VkSampler sampler_ = VK_NULL_HANDLE; ra3::core::uint32 texture_width_ = 0; ra3::core::uint32 texture_height_ = 0; bool terrain_ready_ = false; gpu_image height_texture_; gpu_image tile_texture_; gpu_image atlas_texture_; gpu_image overlay_label_; gpu_image overlay_minimap_; VkDescriptorSet overlay_label_set_ = VK_NULL_HANDLE; VkDescriptorSet overlay_minimap_set_ = VK_NULL_HANDLE; uint32_t overlay_label_w_ = 0U; uint32_t overlay_label_h_ = 0U; uint32_t overlay_minimap_w_ = 0U; uint32_t overlay_minimap_h_ = 0U; VkDescriptorSetLayout terrain_layout_ = VK_NULL_HANDLE; VkPipelineLayout terrain_pipeline_layout_ = VK_NULL_HANDLE; VkPipeline terrain_pipeline_ = VK_NULL_HANDLE; VkDescriptorPool terrain_pool_ = VK_NULL_HANDLE; VkDescriptorSet terrain_set_ = VK_NULL_HANDLE; VkBuffer object_vertex_buffer_ = VK_NULL_HANDLE; VkDeviceMemory object_vertex_memory_ = VK_NULL_HANDLE; VkBuffer object_index_buffer_ = VK_NULL_HANDLE; VkDeviceMemory object_index_memory_ = VK_NULL_HANDLE; uint32_t object_index_count_ = 0U; gpu_image object_texture_; VkPipelineLayout object_pipeline_layout_ = VK_NULL_HANDLE; VkPipeline object_pipeline_ = VK_NULL_HANDLE; VkDescriptorSet object_set_ = VK_NULL_HANDLE; bool objects_ready_ = false; std::vector image_available_; std::vector render_finished_; std::vector in_flight_; uint32_t current_frame_ = 0U; }; }