#include "vulkan_device.h" #include "core/log.h" #define GLFW_INCLUDE_VULKAN #include #include #include #include #include #include #include "stb_image.h" #include #include #include #include #include #include namespace Donut::RHI { namespace { constexpr int MAX_FRAMES_IN_FLIGHT = 2; constexpr uint32_t MAX_BINDINGS = 8; #define VKD_CHECK(expr) \ do { \ VkResult _r = (expr); \ if (_r != VK_SUCCESS) { \ DONUT_ERROR("Vulkan RHI: {} failed ({})", #expr, (int)_r); \ return false; \ } \ } while (0) auto vk_format(Format f) -> VkFormat { switch (f) { case Format::RGBA16F: return VK_FORMAT_R16G16B16A16_SFLOAT; case Format::D32: return VK_FORMAT_D32_SFLOAT; default: return VK_FORMAT_R8G8B8A8_UNORM; } } auto vk_attr_format(uint32_t comps) -> VkFormat { switch (comps) { case 1: return VK_FORMAT_R32_SFLOAT; case 2: return VK_FORMAT_R32G32_SFLOAT; case 3: return VK_FORMAT_R32G32B32_SFLOAT; default: return VK_FORMAT_R32G32B32A32_SFLOAT; } } auto vk_topology(Topology t) -> VkPrimitiveTopology { return t == Topology::Lines ? VK_PRIMITIVE_TOPOLOGY_LINE_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; } auto vk_compare(CompareOp o) -> VkCompareOp { return o == CompareOp::Always ? VK_COMPARE_OP_ALWAYS : o == CompareOp::LessEqual ? VK_COMPARE_OP_LESS_OR_EQUAL : VK_COMPARE_OP_LESS; } auto vk_filter(Filter f) -> VkFilter { return f == Filter::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } auto vk_cull(CullMode c) -> VkCullModeFlags { return c == CullMode::None ? VK_CULL_MODE_NONE : c == CullMode::Back ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_FRONT_BIT; } class VulkanDevice; // ---- Buffer: host-visible + coherent, persistently mapped ----------- class VkBufferR : public Buffer { public: VkBufferR(VkDevice d, VkBuffer b, VkDeviceMemory m, void* mapped, size_t size) : m_device(d), m_buf(b), m_mem(m), m_mapped(mapped), m_size(size) {} ~VkBufferR() override { if (m_mapped) vkUnmapMemory(m_device, m_mem); if (m_buf) vkDestroyBuffer(m_device, m_buf, nullptr); if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); } auto update(const void* data, size_t size) -> void override { if (m_mapped) std::memcpy(m_mapped, data, std::min(size, m_size)); } VkDevice m_device; VkBuffer m_buf; VkDeviceMemory m_mem; void* m_mapped; size_t m_size; }; // ---- Texture: sampled image (2D or cube). Owns its handles unless it is // a borrowed wrapper around a render-target view. ------------------ class VkTextureR : public Texture { public: VkTextureR() = default; ~VkTextureR() override { if (!m_owns) return; if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); if (m_view) vkDestroyImageView(m_device, m_view, nullptr); if (m_image) vkDestroyImage(m_device, m_image, nullptr); if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); } VkDevice m_device = VK_NULL_HANDLE; VkImage m_image = VK_NULL_HANDLE; VkDeviceMemory m_mem = VK_NULL_HANDLE; VkImageView m_view = VK_NULL_HANDLE; VkSampler m_sampler = VK_NULL_HANDLE; bool m_owns = true; }; // ---- RenderTarget: off-screen colour image + framebuffer ------------ class VkRenderTargetR : public RenderTarget { public: ~VkRenderTargetR() override { if (m_fb) vkDestroyFramebuffer(m_device, m_fb, nullptr); if (m_sampler) vkDestroySampler(m_device, m_sampler, nullptr); if (m_view) vkDestroyImageView(m_device, m_view, nullptr); if (m_image) vkDestroyImage(m_device, m_image, nullptr); if (m_mem) vkFreeMemory(m_device, m_mem, nullptr); } auto width() const -> int override { return m_w; } auto height() const -> int override { return m_h; } auto color_texture() -> Texture* override { return &m_color; } VkDevice m_device = VK_NULL_HANDLE; int m_w = 0, m_h = 0; VkImage m_image = VK_NULL_HANDLE; VkDeviceMemory m_mem = VK_NULL_HANDLE; VkImageView m_view = VK_NULL_HANDLE; VkSampler m_sampler = VK_NULL_HANDLE; VkFramebuffer m_fb = VK_NULL_HANDLE; VkTextureR m_color; // borrowed wrapper (view+sampler) for sampling }; // ---- Pipeline ------------------------------------------------------- class VkPipelineR : public Pipeline { public: ~VkPipelineR() override { if (m_pipeline) vkDestroyPipeline(m_device, m_pipeline, nullptr); if (m_layout) vkDestroyPipelineLayout(m_device, m_layout, nullptr); if (m_set_layout) vkDestroyDescriptorSetLayout(m_device, m_set_layout, nullptr); } VkDevice m_device = VK_NULL_HANDLE; VkPipeline m_pipeline = VK_NULL_HANDLE; VkPipelineLayout m_layout = VK_NULL_HANDLE; VkDescriptorSetLayout m_set_layout = VK_NULL_HANDLE; std::vector m_resources; }; // ---- CommandList ---------------------------------------------------- class VkCommandListR : public CommandList { public: auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void override { VkClearValue cvs[2]{}; cvs[0].color = { { clear.r, clear.g, clear.b, clear.a } }; cvs[1].depthStencil = { 1.0f, 0 }; VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; if (target) { auto* rt = static_cast(target); rpbi.renderPass = m_offscreen_rp; rpbi.framebuffer = rt->m_fb; rpbi.renderArea = { { 0, 0 }, { (uint32_t)rt->m_w, (uint32_t)rt->m_h } }; rpbi.clearValueCount = 1; rpbi.pClearValues = cvs; } else { rpbi.renderPass = m_swapchain_rp; rpbi.framebuffer = m_swapchain_fb; rpbi.renderArea = { { 0, 0 }, m_extent }; rpbi.clearValueCount = 2; rpbi.pClearValues = cvs; } vkCmdBeginRenderPass(m_cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); } auto end_render_pass() -> void override { vkCmdEndRenderPass(m_cmd); } auto bind_pipeline(Pipeline* p) -> void override { m_pipe = static_cast(p); vkCmdBindPipeline(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_pipeline); } auto set_viewport(int x, int y, int w, int h, bool flip_y) -> void override { VkViewport vp{ (float)x, flip_y ? (float)(y + h) : (float)y, (float)w, flip_y ? -(float)h : (float)h, 0.0f, 1.0f }; VkRect2D sc{ { x, y }, { (uint32_t)w, (uint32_t)h } }; vkCmdSetViewport(m_cmd, 0, 1, &vp); vkCmdSetScissor(m_cmd, 0, 1, &sc); } auto bind_uniform(uint32_t binding, Buffer* ubo) -> void override { if (binding >= MAX_BINDINGS) return; m_buf_info[binding] = { static_cast(ubo)->m_buf, 0, VK_WHOLE_SIZE }; } auto bind_texture(uint32_t binding, Texture* texture) -> void override { if (binding >= MAX_BINDINGS) return; auto* t = static_cast(texture); m_img_info[binding] = { t->m_sampler, t->m_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; } auto bind_vertex_buffer(Buffer* vb) -> void override { VkBuffer b = static_cast(vb)->m_buf; VkDeviceSize off = 0; vkCmdBindVertexBuffers(m_cmd, 0, 1, &b, &off); } auto bind_index_buffer(Buffer* ib) -> void override { vkCmdBindIndexBuffer(m_cmd, static_cast(ib)->m_buf, 0, VK_INDEX_TYPE_UINT32); } auto draw(uint32_t vertex_count) -> void override { flush_descriptors(); vkCmdDraw(m_cmd, vertex_count, 1, 0, 0); } auto draw_indexed(uint32_t index_count) -> void override { flush_descriptors(); vkCmdDrawIndexed(m_cmd, index_count, 1, 0, 0, 0); } // Allocate + write + bind a descriptor set for the current pipeline's // declared resources, using whatever was bound since bind_pipeline. auto flush_descriptors() -> void { if (!m_pipe || m_pipe->m_resources.empty()) return; VkDescriptorSetAllocateInfo ai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; ai.descriptorPool = m_frame_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &m_pipe->m_set_layout; VkDescriptorSet set = VK_NULL_HANDLE; if (vkAllocateDescriptorSets(m_device, &ai, &set) != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: descriptor set allocation failed"); return; } std::array writes{}; uint32_t n = 0; for (const auto& r : m_pipe->m_resources) { VkWriteDescriptorSet& w = writes[n++]; w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; w.dstSet = set; w.dstBinding = r.binding; w.descriptorCount = 1; if (r.kind == ResourceKind::UniformBuffer) { w.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; w.pBufferInfo = &m_buf_info[r.binding]; } else { w.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; w.pImageInfo = &m_img_info[r.binding]; } } vkUpdateDescriptorSets(m_device, n, writes.data(), 0, nullptr); vkCmdBindDescriptorSets(m_cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipe->m_layout, 0, 1, &set, 0, nullptr); } // Set by the device at begin_frame: VkDevice m_device = VK_NULL_HANDLE; VkCommandBuffer m_cmd = VK_NULL_HANDLE; VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; VkRenderPass m_offscreen_rp = VK_NULL_HANDLE; VkFramebuffer m_swapchain_fb = VK_NULL_HANDLE; VkExtent2D m_extent{}; VkDescriptorPool m_frame_pool = VK_NULL_HANDLE; VkPipelineR* m_pipe = nullptr; VkDescriptorBufferInfo m_buf_info[MAX_BINDINGS]{}; VkDescriptorImageInfo m_img_info[MAX_BINDINGS]{}; }; // ---- Device --------------------------------------------------------- class VulkanDevice : public Device { public: auto init(void* glfwWindow, int width, int height) -> bool override; auto shutdown() -> void override; auto resize(int width, int height) -> void override { m_framebuffer_resized = true; m_width = width; m_height = height; } auto wait_idle() -> void override { if (m_device) vkDeviceWaitIdle(m_device); } auto create_buffer(BufferType type, size_t size, const void* data) -> Ref override; auto create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref override; auto create_cubemap_from_hdri(const std::string& path) -> Ref override; auto create_render_target(int w, int h, Format color, bool with_depth, Filter filter, int mips) -> Ref override; auto create_pipeline(const PipelineDesc& desc) -> Ref override; auto begin_frame(const glm::vec4& clear) -> CommandList* override; auto end_frame() -> void override; auto init_imgui() -> void override; auto imgui_new_frame() -> void override; auto imgui_render(CommandList& cmds) -> void override; auto device_name() const -> const std::string& override { return m_gpu_name; } // --- internals --- auto find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t; auto create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; auto load_spirv(const std::string& path) const -> std::vector; auto create_shader_module(const std::string& path, VkShaderModule& out) const -> bool; auto create_instance() -> bool; auto pick_physical_and_device() -> bool; auto create_swapchain() -> bool; auto create_image_views() -> bool; auto create_swapchain_render_pass() -> bool; auto create_offscreen_render_pass() -> bool; auto create_depth_and_framebuffers() -> bool; auto create_command_and_sync() -> bool; auto recreate_swapchain() -> bool; auto cleanup_swapchain() -> void; GLFWwindow* m_window = nullptr; int m_width = 0, m_height = 0; bool m_framebuffer_resized = false; std::string m_gpu_name; VkInstance m_instance = VK_NULL_HANDLE; VkSurfaceKHR m_surface = VK_NULL_HANDLE; VkPhysicalDevice m_physical = VK_NULL_HANDLE; VkDevice m_device = VK_NULL_HANDLE; uint32_t m_graphics_family = 0, m_present_family = 0; VkQueue m_graphics_queue = VK_NULL_HANDLE, m_present_queue = VK_NULL_HANDLE; VkPhysicalDeviceMemoryProperties m_mem_props{}; VkSwapchainKHR m_swapchain = VK_NULL_HANDLE; VkFormat m_swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; VkExtent2D m_extent{}; std::vector m_images; std::vector m_image_views; VkRenderPass m_swapchain_rp = VK_NULL_HANDLE; VkRenderPass m_offscreen_rp = VK_NULL_HANDLE; std::vector m_framebuffers; VkImage m_depth_image = VK_NULL_HANDLE; VkDeviceMemory m_depth_mem = VK_NULL_HANDLE; VkImageView m_depth_view = VK_NULL_HANDLE; VkCommandPool m_command_pool = VK_NULL_HANDLE; std::vector m_command_buffers; std::vector m_image_available; std::vector m_render_finished; std::vector m_in_flight; std::vector m_images_in_flight; VkFence m_geo_in_use = VK_NULL_HANDLE; uint32_t m_current_frame = 0, m_image_index = 0; std::vector m_frame_pools; // one per frame in flight VkDescriptorPool m_imgui_pool = VK_NULL_HANDLE; bool m_imgui = false; VkCommandListR m_cmds; }; // ================================================================== auto VulkanDevice::find_memory_type(uint32_t filter, VkMemoryPropertyFlags flags) const -> uint32_t { for (uint32_t i = 0; i < m_mem_props.memoryTypeCount; ++i) if ((filter & (1u << i)) && (m_mem_props.memoryTypes[i].propertyFlags & flags) == flags) return i; return UINT32_MAX; } auto VulkanDevice::create_buffer_raw(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool { VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateBuffer(m_device, &bci, nullptr, &buf) != VK_SUCCESS) return false; VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(m_device, buf, &req); VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, props); if (vkAllocateMemory(m_device, &ai, nullptr, &mem) != VK_SUCCESS) return false; vkBindBufferMemory(m_device, buf, mem, 0); return true; } auto VulkanDevice::load_spirv(const std::string& path) const -> std::vector { std::ifstream file(path, std::ios::ate | std::ios::binary); if (!file.is_open()) return {}; size_t size = (size_t)file.tellg(); std::vector data(size / 4); file.seekg(0); file.read(reinterpret_cast(data.data()), size); return data; } auto VulkanDevice::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool { auto spv = load_spirv(path); if (spv.empty()) { DONUT_ERROR("Vulkan RHI: failed to load SPIR-V {}", path); return false; } VkShaderModuleCreateInfo ci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; ci.codeSize = spv.size() * 4; ci.pCode = spv.data(); return vkCreateShaderModule(m_device, &ci, nullptr, &out) == VK_SUCCESS; } auto VulkanDevice::create_instance() -> bool { VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; app.pApplicationName = "Donut"; app.apiVersion = VK_API_VERSION_1_2; uint32_t glfwExtCount = 0; const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount); if (!glfwExts) { DONUT_ERROR("Vulkan RHI: GLFW reports no surface support"); return false; } std::vector exts(glfwExts, glfwExts + glfwExtCount); exts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); exts.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); std::vector layers; uint32_t layer_count = 0; vkEnumerateInstanceLayerProperties(&layer_count, nullptr); std::vector avail(layer_count); vkEnumerateInstanceLayerProperties(&layer_count, avail.data()); for (const auto& l : avail) if (std::strcmp(l.layerName, "VK_LAYER_KHRONOS_validation") == 0) layers.push_back("VK_LAYER_KHRONOS_validation"); VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; ici.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; ici.pApplicationInfo = &app; ici.enabledExtensionCount = (uint32_t)exts.size(); ici.ppEnabledExtensionNames = exts.data(); ici.enabledLayerCount = (uint32_t)layers.size(); ici.ppEnabledLayerNames = layers.data(); VkResult r = vkCreateInstance(&ici, nullptr, &m_instance); if (r != VK_SUCCESS && !layers.empty()) { DONUT_WARN("Vulkan RHI: validation layer unavailable, continuing without it"); ici.enabledLayerCount = 0; ici.ppEnabledLayerNames = nullptr; r = vkCreateInstance(&ici, nullptr, &m_instance); } if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: vkCreateInstance failed ({})", (int)r); return false; } VKD_CHECK(glfwCreateWindowSurface(m_instance, m_window, nullptr, &m_surface)); DONUT_INFO("Vulkan RHI: instance + surface created (validation {})", layers.empty() ? "off" : "on"); return true; } auto VulkanDevice::pick_physical_and_device() -> bool { uint32_t count = 0; vkEnumeratePhysicalDevices(m_instance, &count, nullptr); if (count == 0) { DONUT_ERROR("Vulkan RHI: no physical devices"); return false; } std::vector devices(count); vkEnumeratePhysicalDevices(m_instance, &count, devices.data()); m_physical = devices[0]; uint32_t q = 0; vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, nullptr); std::vector qfams(q); vkGetPhysicalDeviceQueueFamilyProperties(m_physical, &q, qfams.data()); bool fg = false, fp = false; for (uint32_t i = 0; i < q; ++i) { if (!fg && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { m_graphics_family = i; fg = true; } VkBool32 present = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(m_physical, i, m_surface, &present); if (!fp && present) { m_present_family = i; fp = true; } } if (!fg || !fp) { DONUT_ERROR("Vulkan RHI: no graphics/present queue"); return false; } std::vector dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, nullptr); std::vector dep(dec); vkEnumerateDeviceExtensionProperties(m_physical, nullptr, &dec, dep.data()); for (const auto& e : dep) if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) dev_exts.push_back("VK_KHR_portability_subset"); float priority = 1.0f; std::vector qcis; uint32_t families[2] = { m_graphics_family, m_present_family }; for (uint32_t i = 0; i < (m_graphics_family == m_present_family ? 1u : 2u); ++i) { VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; qci.queueFamilyIndex = families[i]; qci.queueCount = 1; qci.pQueuePriorities = &priority; qcis.push_back(qci); } VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; dci.queueCreateInfoCount = (uint32_t)qcis.size(); dci.pQueueCreateInfos = qcis.data(); dci.enabledExtensionCount = (uint32_t)dev_exts.size(); dci.ppEnabledExtensionNames = dev_exts.data(); VKD_CHECK(vkCreateDevice(m_physical, &dci, nullptr, &m_device)); vkGetDeviceQueue(m_device, m_graphics_family, 0, &m_graphics_queue); vkGetDeviceQueue(m_device, m_present_family, 0, &m_present_queue); VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(m_physical, &props); vkGetPhysicalDeviceMemoryProperties(m_physical, &m_mem_props); m_gpu_name = props.deviceName; DONUT_INFO("Vulkan RHI device: {}", m_gpu_name); return true; } auto VulkanDevice::create_swapchain() -> bool { VkSurfaceCapabilitiesKHR caps{}; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_physical, m_surface, &caps); uint32_t fc = 0; vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, nullptr); std::vector formats(fc); vkGetPhysicalDeviceSurfaceFormatsKHR(m_physical, m_surface, &fc, formats.data()); VkSurfaceFormatKHR chosen = formats[0]; for (const auto& f : formats) if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen = f; m_swapchain_format = chosen.format; if (caps.currentExtent.width != UINT32_MAX) m_extent = caps.currentExtent; else { m_extent.width = std::clamp((uint32_t)m_width, caps.minImageExtent.width, caps.maxImageExtent.width); m_extent.height = std::clamp((uint32_t)m_height, caps.minImageExtent.height, caps.maxImageExtent.height); } uint32_t image_count = caps.minImageCount + 1; if (caps.maxImageCount > 0 && image_count > caps.maxImageCount) image_count = caps.maxImageCount; VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; sci.surface = m_surface; sci.minImageCount = image_count; sci.imageFormat = chosen.format; sci.imageColorSpace = chosen.colorSpace; sci.imageExtent = m_extent; sci.imageArrayLayers = 1; sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; sci.preTransform = caps.currentTransform; sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; sci.clipped = VK_TRUE; uint32_t fam[2] = { m_graphics_family, m_present_family }; if (m_graphics_family != m_present_family) { sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; sci.queueFamilyIndexCount = 2; sci.pQueueFamilyIndices = fam; } else sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; VKD_CHECK(vkCreateSwapchainKHR(m_device, &sci, nullptr, &m_swapchain)); uint32_t n = 0; vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, nullptr); m_images.resize(n); vkGetSwapchainImagesKHR(m_device, m_swapchain, &n, m_images.data()); return true; } auto VulkanDevice::create_image_views() -> bool { m_image_views.resize(m_images.size()); for (size_t i = 0; i < m_images.size(); ++i) { VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; vci.image = m_images[i]; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = m_swapchain_format; vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; VKD_CHECK(vkCreateImageView(m_device, &vci, nullptr, &m_image_views[i])); } return true; } // Swapchain pass: colour + depth. Scene geometry uses the depth; the // black-hole present + ImGui simply don't test it. auto VulkanDevice::create_swapchain_render_pass() -> bool { VkAttachmentDescription atts[2]{}; atts[0].format = m_swapchain_format; atts[0].samples = VK_SAMPLE_COUNT_1_BIT; atts[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; atts[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; atts[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; atts[1].format = VK_FORMAT_D32_SFLOAT; atts[1].samples = VK_SAMPLE_COUNT_1_BIT; atts[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; atts[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; atts[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; atts[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; atts[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; atts[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; VkAttachmentReference depth_ref{ 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_ref; subpass.pDepthStencilAttachment = &depth_ref; VkSubpassDependency dep{}; dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; rpci.attachmentCount = 2; rpci.pAttachments = atts; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep; VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_swapchain_rp)); return true; } // Off-screen colour pass (RGBA8), leaving the image SHADER_READ_ONLY so the // present pass can sample it. Shared by every render target. auto VulkanDevice::create_offscreen_render_pass() -> bool { VkAttachmentDescription color{}; color.format = VK_FORMAT_R8G8B8A8_UNORM; 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_SHADER_READ_ONLY_OPTIMAL; VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; VkSubpassDependency deps[2]{}; deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; deps[0].dstSubpass = 0; deps[0].srcStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT; deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; deps[1].srcSubpass = 0; deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 2; rpci.pDependencies = deps; VKD_CHECK(vkCreateRenderPass(m_device, &rpci, nullptr, &m_offscreen_rp)); return true; } auto VulkanDevice::create_depth_and_framebuffers() -> bool { VkImageCreateInfo dici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; dici.imageType = VK_IMAGE_TYPE_2D; dici.format = VK_FORMAT_D32_SFLOAT; dici.extent = { m_extent.width, m_extent.height, 1 }; dici.mipLevels = 1; dici.arrayLayers = 1; dici.samples = VK_SAMPLE_COUNT_1_BIT; dici.tiling = VK_IMAGE_TILING_OPTIMAL; dici.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; VKD_CHECK(vkCreateImage(m_device, &dici, nullptr, &m_depth_image)); VkMemoryRequirements dreq{}; vkGetImageMemoryRequirements(m_device, m_depth_image, &dreq); VkMemoryAllocateInfo dai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; dai.allocationSize = dreq.size; dai.memoryTypeIndex = find_memory_type(dreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); VKD_CHECK(vkAllocateMemory(m_device, &dai, nullptr, &m_depth_mem)); VKD_CHECK(vkBindImageMemory(m_device, m_depth_image, m_depth_mem, 0)); VkImageViewCreateInfo dvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; dvci.image = m_depth_image; dvci.viewType = VK_IMAGE_VIEW_TYPE_2D; dvci.format = VK_FORMAT_D32_SFLOAT; dvci.subresourceRange = { VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1 }; VKD_CHECK(vkCreateImageView(m_device, &dvci, nullptr, &m_depth_view)); m_framebuffers.resize(m_image_views.size()); for (size_t i = 0; i < m_image_views.size(); ++i) { VkImageView att[2] = { m_image_views[i], m_depth_view }; VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; fbci.renderPass = m_swapchain_rp; fbci.attachmentCount = 2; fbci.pAttachments = att; fbci.width = m_extent.width; fbci.height = m_extent.height; fbci.layers = 1; VKD_CHECK(vkCreateFramebuffer(m_device, &fbci, nullptr, &m_framebuffers[i])); } return true; } auto VulkanDevice::create_command_and_sync() -> bool { VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = m_graphics_family; VKD_CHECK(vkCreateCommandPool(m_device, &pci, nullptr, &m_command_pool)); m_command_buffers.resize(MAX_FRAMES_IN_FLIGHT); VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; VKD_CHECK(vkAllocateCommandBuffers(m_device, &cbai, m_command_buffers.data())); m_image_available.resize(MAX_FRAMES_IN_FLIGHT); m_in_flight.resize(MAX_FRAMES_IN_FLIGHT); m_render_finished.resize(m_images.size()); m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); VkSemaphoreCreateInfo sci{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_image_available[i])); VKD_CHECK(vkCreateFence(m_device, &fci, nullptr, &m_in_flight[i])); } for (size_t i = 0; i < m_images.size(); ++i) VKD_CHECK(vkCreateSemaphore(m_device, &sci, nullptr, &m_render_finished[i])); m_frame_pools.resize(MAX_FRAMES_IN_FLIGHT); VkDescriptorPoolSize sizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256 }, }; for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) { VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 256; dpci.poolSizeCount = 2; dpci.pPoolSizes = sizes; VKD_CHECK(vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_frame_pools[i])); } return true; } auto VulkanDevice::init(void* glfwWindow, int width, int height) -> bool { m_window = (GLFWwindow*)glfwWindow; m_width = width; m_height = height; if (!create_instance()) return false; if (!pick_physical_and_device()) return false; if (!create_swapchain()) return false; if (!create_image_views()) return false; if (!create_swapchain_render_pass()) return false; if (!create_offscreen_render_pass()) return false; if (!create_depth_and_framebuffers())return false; if (!create_command_and_sync()) return false; DONUT_INFO("Vulkan RHI device ready: {} swapchain images, {}x{}", (int)m_images.size(), m_extent.width, m_extent.height); return true; } auto VulkanDevice::cleanup_swapchain() -> void { for (auto fb : m_framebuffers) vkDestroyFramebuffer(m_device, fb, nullptr); m_framebuffers.clear(); if (m_depth_view) { vkDestroyImageView(m_device, m_depth_view, nullptr); m_depth_view = VK_NULL_HANDLE; } if (m_depth_image) { vkDestroyImage(m_device, m_depth_image, nullptr); m_depth_image = VK_NULL_HANDLE; } if (m_depth_mem) { vkFreeMemory(m_device, m_depth_mem, nullptr); m_depth_mem = VK_NULL_HANDLE; } for (auto iv : m_image_views) vkDestroyImageView(m_device, iv, nullptr); m_image_views.clear(); if (m_swapchain) { vkDestroySwapchainKHR(m_device, m_swapchain, nullptr); m_swapchain = VK_NULL_HANDLE; } } auto VulkanDevice::recreate_swapchain() -> bool { int w = 0, h = 0; glfwGetFramebufferSize(m_window, &w, &h); while (w == 0 || h == 0) { glfwGetFramebufferSize(m_window, &w, &h); glfwWaitEvents(); } m_width = w; m_height = h; vkDeviceWaitIdle(m_device); cleanup_swapchain(); if (!create_swapchain()) return false; if (!create_image_views()) return false; if (!create_depth_and_framebuffers())return false; m_images_in_flight.assign(m_images.size(), VK_NULL_HANDLE); return true; } auto VulkanDevice::create_buffer(BufferType type, size_t size, const void* data) -> Ref { VkBufferUsageFlags usage = type == BufferType::Index ? VK_BUFFER_USAGE_INDEX_BUFFER_BIT : type == BufferType::Uniform ? VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT : VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; create_buffer_raw(size, usage, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, buf, mem); void* mapped = nullptr; vkMapMemory(m_device, mem, 0, size, 0, &mapped); if (data && mapped) std::memcpy(mapped, data, size); return create_ref(m_device, buf, mem, mapped, size); } auto VulkanDevice::create_texture(int w, int h, Format format, Filter filter, const void* data) -> Ref { auto tex = create_ref(); tex->m_device = m_device; VkFormat fmt = vk_format(format); VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; vkCreateImage(m_device, &ici, nullptr, &tex->m_image); VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &req); VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); vkAllocateMemory(m_device, &ai, nullptr, &tex->m_mem); vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); size_t bpp = format == Format::RGBA16F ? 8 : 4; VkDeviceSize sz = (VkDeviceSize)w * h * bpp; VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; create_buffer_raw(sz, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, staging, staging_mem); void* mp = nullptr; vkMapMemory(m_device, staging_mem, 0, sz, 0, &mp); if (data) std::memcpy(mp, data, sz); else std::memset(mp, 0, sz); vkUnmapMemory(m_device, staging_mem); VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmd, &bi); VkImageMemoryBarrier b{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; b.image = tex->m_image; b.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; b.srcAccessMask = 0; b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; vkCmdCopyBufferToImage(cmd, staging, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); VkImageMemoryBarrier r = b; r.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; r.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; r.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; r.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &r); vkEndCommandBuffer(cmd); VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); vkDestroyBuffer(m_device, staging, nullptr); vkFreeMemory(m_device, staging_mem, nullptr); VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; vci.image = tex->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; vkCreateImageView(m_device, &vci, nullptr, &tex->m_view); VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; vkCreateSampler(m_device, &smci, nullptr, &tex->m_sampler); return tex; } auto VulkanDevice::create_render_target(int w, int h, Format color, bool /*with_depth*/, Filter filter, int /*mips*/) -> Ref { auto rt = create_ref(); rt->m_device = m_device; rt->m_w = w; rt->m_h = h; VkFormat fmt = vk_format(color); VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { (uint32_t)w, (uint32_t)h, 1 }; ici.mipLevels = 1; ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.tiling = VK_IMAGE_TILING_OPTIMAL; ici.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; vkCreateImage(m_device, &ici, nullptr, &rt->m_image); VkMemoryRequirements req{}; vkGetImageMemoryRequirements(m_device, rt->m_image, &req); VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; ai.allocationSize = req.size; ai.memoryTypeIndex = find_memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); vkAllocateMemory(m_device, &ai, nullptr, &rt->m_mem); vkBindImageMemory(m_device, rt->m_image, rt->m_mem, 0); VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; vci.image = rt->m_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; vkCreateImageView(m_device, &vci, nullptr, &rt->m_view); VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; smci.magFilter = vk_filter(filter); smci.minFilter = vk_filter(filter); smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; vkCreateSampler(m_device, &smci, nullptr, &rt->m_sampler); VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; fbci.renderPass = m_offscreen_rp; fbci.attachmentCount = 1; fbci.pAttachments = &rt->m_view; fbci.width = w; fbci.height = h; fbci.layers = 1; vkCreateFramebuffer(m_device, &fbci, nullptr, &rt->m_fb); rt->m_color.m_device = m_device; rt->m_color.m_view = rt->m_view; rt->m_color.m_sampler = rt->m_sampler; rt->m_color.m_owns = false; return rt; } // Builds an environment cubemap from an equirectangular HDRI: render the 6 // faces with the EquirectToCubemap pipeline, then a full mip chain by // linear down-blits (so divergence-based LOD reads a blurred sky). Returns // a Texture owning the cube image/view/sampler. auto VulkanDevice::create_cubemap_from_hdri(const std::string& path) -> Ref { auto tex = create_ref(); tex->m_device = m_device; const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; const uint32_t FACE = 1024; const VkFormat cube_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; uint32_t CUBE_MIPS = 1; for (uint32_t s = FACE; s > 1; s >>= 1) ++CUBE_MIPS; VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; cci.imageType = VK_IMAGE_TYPE_2D; cci.format = cube_fmt; cci.extent = { FACE, FACE, 1 }; cci.mipLevels = CUBE_MIPS; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; cci.tiling = VK_IMAGE_TILING_OPTIMAL; cci.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; vkCreateImage(m_device, &cci, nullptr, &tex->m_image); VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(m_device, tex->m_image, &creq); VkMemoryAllocateInfo cai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; cai.allocationSize = creq.size; cai.memoryTypeIndex = find_memory_type(creq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); vkAllocateMemory(m_device, &cai, nullptr, &tex->m_mem); vkBindImageMemory(m_device, tex->m_image, tex->m_mem, 0); VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; cvci.image = tex->m_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; vkCreateImageView(m_device, &cvci, nullptr, &tex->m_view); VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR; csm.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; csm.minLod = 0.0f; csm.maxLod = (float)CUBE_MIPS; csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; vkCreateSampler(m_device, &csm, nullptr, &tex->m_sampler); int w = 0, h = 0, ch = 0; float* pixels = stbi_loadf(path.c_str(), &w, &h, &ch, 4); if (!pixels) { DONUT_WARN("Vulkan RHI: HDRI '{}' could not be loaded; using a dark background", path); VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmd, &bi); VkImageMemoryBarrier tb{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; tb.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; tb.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; tb.image = tex->m_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; tb.srcAccessMask = 0; tb.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &tb); VkClearColorValue dark{}; dark.float32[0] = 0.02f; dark.float32[1] = 0.02f; dark.float32[2] = 0.05f; dark.float32[3] = 1.0f; VkImageSubresourceRange rng{ VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; vkCmdClearColorImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &dark, 1, &rng); VkImageMemoryBarrier rb = tb; rb.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; rb.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; rb.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; rb.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &rb); vkEndCommandBuffer(cmd); VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); return tex; } const VkFormat eq_fmt = VK_FORMAT_R16G16B16A16_SFLOAT; size_t texel_count = (size_t)w * h * 4; VkDeviceSize eq_size = (VkDeviceSize)texel_count * sizeof(uint16_t); VkBuffer eq_staging; VkDeviceMemory eq_staging_mem; create_buffer_raw(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem); void* mp = nullptr; vkMapMemory(m_device, eq_staging_mem, 0, eq_size, 0, &mp); uint16_t* dst = (uint16_t*)mp; for (size_t i = 0; i < texel_count; ++i) { __fp16 hf = (__fp16)pixels[i]; std::memcpy(&dst[i], &hf, sizeof(uint16_t)); } vkUnmapMemory(m_device, eq_staging_mem); stbi_image_free(pixels); VkImage eq_image; VkDeviceMemory eq_mem; VkImageCreateInfo eci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; eci.imageType = VK_IMAGE_TYPE_2D; eci.format = eq_fmt; eci.extent = { (uint32_t)w, (uint32_t)h, 1 }; eci.mipLevels = 1; eci.arrayLayers = 1; eci.samples = VK_SAMPLE_COUNT_1_BIT; eci.tiling = VK_IMAGE_TILING_OPTIMAL; eci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; vkCreateImage(m_device, &eci, nullptr, &eq_image); VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(m_device, eq_image, &ereq); VkMemoryAllocateInfo eai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; eai.allocationSize = ereq.size; eai.memoryTypeIndex = find_memory_type(ereq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); vkAllocateMemory(m_device, &eai, nullptr, &eq_mem); vkBindImageMemory(m_device, eq_image, eq_mem, 0); VkImageView eq_view; VkImageViewCreateInfo evci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; evci.image = eq_image; evci.viewType = VK_IMAGE_VIEW_TYPE_2D; evci.format = eq_fmt; evci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; vkCreateImageView(m_device, &evci, nullptr, &eq_view); VkSampler eq_sampler; VkSamplerCreateInfo esm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; esm.magFilter = VK_FILTER_LINEAR; esm.minFilter = VK_FILTER_LINEAR; esm.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; vkCreateSampler(m_device, &esm, nullptr, &eq_sampler); VkImageView face_views[6]; for (uint32_t i = 0; i < 6; ++i) { VkImageViewCreateInfo fvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; fvci.image = tex->m_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; vkCreateImageView(m_device, &fvci, nullptr, &face_views[i]); } VkAttachmentDescription color{}; color.format = cube_fmt; 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_SHADER_READ_ONLY_OPTIMAL; VkAttachmentReference ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &ref; VkSubpassDependency dep{}; dep.srcSubpass = 0; dep.dstSubpass = VK_SUBPASS_EXTERNAL; dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; dep.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; dep.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; VkRenderPass rp; VkRenderPassCreateInfo rpci{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; rpci.attachmentCount = 1; rpci.pAttachments = &color; rpci.subpassCount = 1; rpci.pSubpasses = &subpass; rpci.dependencyCount = 1; rpci.pDependencies = &dep; vkCreateRenderPass(m_device, &rpci, nullptr, &rp); VkFramebuffer face_fb[6]; for (uint32_t i = 0; i < 6; ++i) { VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; fbci.renderPass = rp; fbci.attachmentCount = 1; fbci.pAttachments = &face_views[i]; fbci.width = FACE; fbci.height = FACE; fbci.layers = 1; vkCreateFramebuffer(m_device, &fbci, nullptr, &face_fb[i]); } VkDescriptorSetLayoutBinding binds[2]{}; binds[0].binding = 0; binds[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[0].descriptorCount = 1; binds[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; binds[1].binding = 1; binds[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[1].descriptorCount = 1; binds[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; VkDescriptorSetLayout set_layout; VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = 2; dslci.pBindings = binds; vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &set_layout); VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 6 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6 } }; VkDescriptorPool pool; VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.maxSets = 6; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; vkCreateDescriptorPool(m_device, &dpci, nullptr, &pool); VkShaderModule vmod, fmod; create_shader_module("assets/shaders/generated/equirect_to_cubemap.vertexMain.spv", vmod); create_shader_module("assets/shaders/generated/equirect_to_cubemap.fragmentMain.spv", fmod); VkPipelineLayout playout; VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; vkCreatePipelineLayout(m_device, &plci, nullptr, &playout); 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 = vmod; 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 = fmod; stages[1].pName = "main"; VkVertexInputBindingDescription vib{ 0, 12, VK_VERTEX_INPUT_RATE_VERTEX }; VkVertexInputAttributeDescription via{ 0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0 }; VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = 1; vin.pVertexAttributeDescriptions = &via; VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkViewport vp{ 0, 0, (float)FACE, (float)FACE, 0, 1 }; VkRect2D sc{ { 0, 0 }, { FACE, FACE } }; VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = ≻ VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; VkPipeline pipeline; VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; gpci.stageCount = 2; gpci.pStages = stages; gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.layout = playout; gpci.renderPass = rp; gpci.subpass = 0; vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); float cube_verts[] = { -1,1,-1, -1,-1,-1, 1,-1,-1, 1,-1,-1, 1,1,-1, -1,1,-1, -1,-1,1, -1,-1,-1, -1,1,-1, -1,1,-1, -1,1,1, -1,-1,1, 1,-1,-1, 1,-1,1, 1,1,1, 1,1,1, 1,1,-1, 1,-1,-1, -1,-1,1, -1,1,1, 1,1,1, 1,1,1, 1,-1,1, -1,-1,1, -1,1,-1, 1,1,-1, 1,1,1, 1,1,1, -1,1,1, -1,1,-1, -1,-1,-1, -1,-1,1, 1,-1,-1, 1,-1,-1, -1,-1,1, 1,-1,1, }; VkBuffer cube_vb; VkDeviceMemory cube_vb_mem; create_buffer_raw(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); vkMapMemory(m_device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); std::memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(m_device, cube_vb_mem); glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); proj[1][1] *= -1.0f; glm::mat4 views[6] = { glm::lookAt(glm::vec3(0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), glm::lookAt(glm::vec3(0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), glm::lookAt(glm::vec3(0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), glm::lookAt(glm::vec3(0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), glm::lookAt(glm::vec3(0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)), }; VkBuffer ubo[6]; VkDeviceMemory ubo_mem[6]; VkDescriptorSet sets[6]; for (uint32_t i = 0; i < 6; ++i) { create_buffer_raw(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, ubo[i], ubo_mem[i]); glm::mat4 mats[2] = { glm::transpose(proj), glm::transpose(views[i]) }; vkMapMemory(m_device, ubo_mem[i], 0, 128, 0, &mp); std::memcpy(mp, mats, 128); vkUnmapMemory(m_device, ubo_mem[i]); VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; vkAllocateDescriptorSets(m_device, &dsai, &sets[i]); VkDescriptorBufferInfo buf_info{ ubo[i], 0, VK_WHOLE_SIZE }; VkDescriptorImageInfo img_info{ eq_sampler, eq_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; VkWriteDescriptorSet ws[2]{}; ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[0].dstSet = sets[i]; ws[0].dstBinding = 0; ws[0].descriptorCount = 1; ws[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; ws[0].pBufferInfo = &buf_info; ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; ws[1].dstSet = sets[i]; ws[1].dstBinding = 1; ws[1].descriptorCount = 1; ws[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ws[1].pImageInfo = &img_info; vkUpdateDescriptorSets(m_device, 2, ws, 0, nullptr); } VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = m_command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; VkCommandBuffer cmd; vkAllocateCommandBuffers(m_device, &cbai, &cmd); VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmd, &bi); VkImageMemoryBarrier to_dst{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_dst.image = eq_image; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; to_dst.srcAccessMask = 0; to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_dst); VkBufferImageCopy copy{}; copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.imageExtent = { (uint32_t)w, (uint32_t)h, 1 }; vkCmdCopyBufferToImage(cmd, eq_staging, eq_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); VkImageMemoryBarrier to_read = to_dst; to_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; to_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_read); VkClearValue clear{}; clear.color = { { 0, 0, 0, 1 } }; for (uint32_t i = 0; i < 6; ++i) { VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; rpbi.renderPass = rp; rpbi.framebuffer = face_fb[i]; rpbi.renderArea = { { 0, 0 }, { FACE, FACE } }; rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, playout, 0, 1, &sets[i], 0, nullptr); VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &cube_vb, &off); vkCmdDraw(cmd, 36, 1, 0, 0); vkCmdEndRenderPass(cmd); } { VkImageMemoryBarrier src0{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; src0.image = tex->m_image; src0.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; src0.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; src0.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; src0.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; src0.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src0); int32_t mipW = (int32_t)FACE, mipH = (int32_t)FACE; for (uint32_t m = 1; m < CUBE_MIPS; ++m) { int32_t nW = mipW > 1 ? mipW / 2 : 1, nH = mipH > 1 ? mipH / 2 : 1; VkImageMemoryBarrier bd{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; bd.image = tex->m_image; bd.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; bd.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; bd.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bd.srcAccessMask = 0; bd.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bd); VkImageBlit blit{}; blit.srcOffsets[1] = { mipW, mipH, 1 }; blit.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m - 1, 0, 6 }; blit.dstOffsets[1] = { nW, nH, 1 }; blit.dstSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, m, 0, 6 }; vkCmdBlitImage(cmd, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, tex->m_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); VkImageMemoryBarrier bs{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; bs.image = tex->m_image; bs.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, m, 1, 0, 6 }; bs.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; bs.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; bs.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; bs.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bs); mipW = nW; mipH = nH; } VkImageMemoryBarrier fin{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; fin.image = tex->m_image; fin.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, CUBE_MIPS, 0, 6 }; fin.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; fin.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; fin.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; fin.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &fin); } vkEndCommandBuffer(cmd); VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; vkQueueSubmit(m_graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(m_graphics_queue); vkFreeCommandBuffers(m_device, m_command_pool, 1, &cmd); for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(m_device, ubo[i], nullptr); vkFreeMemory(m_device, ubo_mem[i], nullptr); vkDestroyFramebuffer(m_device, face_fb[i], nullptr); vkDestroyImageView(m_device, face_views[i], nullptr); } vkDestroyBuffer(m_device, cube_vb, nullptr); vkFreeMemory(m_device, cube_vb_mem, nullptr); vkDestroyPipeline(m_device, pipeline, nullptr); vkDestroyPipelineLayout(m_device, playout, nullptr); vkDestroyDescriptorPool(m_device, pool, nullptr); vkDestroyDescriptorSetLayout(m_device, set_layout, nullptr); vkDestroyRenderPass(m_device, rp, nullptr); vkDestroySampler(m_device, eq_sampler, nullptr); vkDestroyImageView(m_device, eq_view, nullptr); vkDestroyImage(m_device, eq_image, nullptr); vkFreeMemory(m_device, eq_mem, nullptr); vkDestroyBuffer(m_device, eq_staging, nullptr); vkFreeMemory(m_device, eq_staging_mem, nullptr); DONUT_INFO("Vulkan RHI: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); return tex; } auto VulkanDevice::create_pipeline(const PipelineDesc& desc) -> Ref { auto p = create_ref(); p->m_device = m_device; p->m_resources = desc.resources; std::vector binds; for (const auto& r : desc.resources) { VkDescriptorSetLayoutBinding b{}; b.binding = r.binding; b.descriptorCount = 1; b.descriptorType = r.kind == ResourceKind::UniformBuffer ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; b.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; binds.push_back(b); } VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; dslci.bindingCount = (uint32_t)binds.size(); dslci.pBindings = binds.data(); vkCreateDescriptorSetLayout(m_device, &dslci, nullptr, &p->m_set_layout); VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &p->m_set_layout; vkCreatePipelineLayout(m_device, &plci, nullptr, &p->m_layout); VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; if (!create_shader_module("assets/shaders/generated/" + desc.shader + ".vertexMain.spv", vmod) || !create_shader_module("assets/shaders/generated/" + desc.shader + ".fragmentMain.spv", fmod)) { DONUT_ERROR("Vulkan RHI: shader '{}' modules failed", desc.shader); return p; } 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 = vmod; 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 = fmod; stages[1].pName = "main"; VkVertexInputBindingDescription vib{ 0, desc.vertex_layout.stride, VK_VERTEX_INPUT_RATE_VERTEX }; std::vector vias; for (const auto& a : desc.vertex_layout.attributes) vias.push_back({ a.location, 0, vk_attr_format(a.components), a.offset }); VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; vin.vertexBindingDescriptionCount = desc.vertex_layout.stride ? 1 : 0; vin.pVertexBindingDescriptions = &vib; vin.vertexAttributeDescriptionCount = (uint32_t)vias.size(); vin.pVertexAttributeDescriptions = vias.data(); VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = vk_topology(desc.topology); VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; vps.viewportCount = 1; vps.scissorCount = 1; VkDynamicState dyn[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; VkPipelineDynamicStateCreateInfo dsci{ VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO }; dsci.dynamicStateCount = 2; dsci.pDynamicStates = dyn; VkPipelineRasterizationStateCreateInfo rs{ VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO }; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = vk_cull(desc.cull); rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; VkPipelineMultisampleStateCreateInfo ms{ VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO }; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo ds{ VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; ds.depthTestEnable = desc.depth_test ? VK_TRUE : VK_FALSE; ds.depthWriteEnable = desc.depth_write ? VK_TRUE : VK_FALSE; ds.depthCompareOp = vk_compare(desc.depth_op); VkPipelineColorBlendAttachmentState cba{}; cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; if (desc.blend == BlendMode::AlphaBlend) { cba.blendEnable = VK_TRUE; cba.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; cba.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; cba.colorBlendOp = VK_BLEND_OP_ADD; cba.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; cba.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; cba.alphaBlendOp = VK_BLEND_OP_ADD; } VkPipelineColorBlendStateCreateInfo cb{ VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO }; cb.attachmentCount = 1; cb.pAttachments = &cba; VkGraphicsPipelineCreateInfo gpci{ VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO }; gpci.stageCount = 2; gpci.pStages = stages; gpci.pVertexInputState = &vin; gpci.pInputAssemblyState = &ia; gpci.pViewportState = &vps; gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; gpci.pDynamicState = &dsci; if (desc.has_depth) gpci.pDepthStencilState = &ds; gpci.layout = p->m_layout; gpci.renderPass = desc.has_depth ? m_swapchain_rp : m_offscreen_rp; gpci.subpass = 0; VkResult pr = vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpci, nullptr, &p->m_pipeline); vkDestroyShaderModule(m_device, vmod, nullptr); vkDestroyShaderModule(m_device, fmod, nullptr); if (pr != VK_SUCCESS) DONUT_ERROR("Vulkan RHI: pipeline '{}' creation failed ({})", desc.shader, (int)pr); return p; } auto VulkanDevice::begin_frame(const glm::vec4&) -> CommandList* { if (m_device == VK_NULL_HANDLE) return nullptr; vkWaitForFences(m_device, 1, &m_in_flight[m_current_frame], VK_TRUE, UINT64_MAX); VkResult r = vkAcquireNextImageKHR(m_device, m_swapchain, UINT64_MAX, m_image_available[m_current_frame], VK_NULL_HANDLE, &m_image_index); if (r == VK_ERROR_OUT_OF_DATE_KHR) { recreate_swapchain(); return nullptr; } if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan RHI: acquire failed ({})", (int)r); return nullptr; } if (m_images_in_flight[m_image_index] != VK_NULL_HANDLE) vkWaitForFences(m_device, 1, &m_images_in_flight[m_image_index], VK_TRUE, UINT64_MAX); m_images_in_flight[m_image_index] = m_in_flight[m_current_frame]; if (m_geo_in_use != VK_NULL_HANDLE) vkWaitForFences(m_device, 1, &m_geo_in_use, VK_TRUE, UINT64_MAX); vkResetDescriptorPool(m_device, m_frame_pools[m_current_frame], 0); VkCommandBuffer cmd = m_command_buffers[m_current_frame]; vkResetCommandBuffer(cmd, 0); VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; vkBeginCommandBuffer(cmd, &bi); m_cmds.m_device = m_device; m_cmds.m_cmd = cmd; m_cmds.m_swapchain_rp = m_swapchain_rp; m_cmds.m_offscreen_rp = m_offscreen_rp; m_cmds.m_swapchain_fb = m_framebuffers[m_image_index]; m_cmds.m_extent = m_extent; m_cmds.m_frame_pool = m_frame_pools[m_current_frame]; m_cmds.m_pipe = nullptr; return &m_cmds; } auto VulkanDevice::end_frame() -> void { VkCommandBuffer cmd = m_command_buffers[m_current_frame]; vkEndCommandBuffer(cmd); VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &m_image_available[m_current_frame]; submit.pWaitDstStageMask = &wait_stage; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &m_render_finished[m_image_index]; vkResetFences(m_device, 1, &m_in_flight[m_current_frame]); if (vkQueueSubmit(m_graphics_queue, 1, &submit, m_in_flight[m_current_frame]) != VK_SUCCESS) { DONUT_ERROR("Vulkan RHI: queue submit failed"); return; } m_geo_in_use = m_in_flight[m_current_frame]; VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; present.waitSemaphoreCount = 1; present.pWaitSemaphores = &m_render_finished[m_image_index]; present.swapchainCount = 1; present.pSwapchains = &m_swapchain; present.pImageIndices = &m_image_index; VkResult r = vkQueuePresentKHR(m_present_queue, &present); if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || m_framebuffer_resized) { m_framebuffer_resized = false; recreate_swapchain(); } m_current_frame = (m_current_frame + 1) % MAX_FRAMES_IN_FLIGHT; } auto VulkanDevice::init_imgui() -> void { VkDescriptorPoolSize pool_size{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }; VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; dpci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; dpci.maxSets = 1000; dpci.poolSizeCount = 1; dpci.pPoolSizes = &pool_size; vkCreateDescriptorPool(m_device, &dpci, nullptr, &m_imgui_pool); IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForVulkan(m_window, true); ImGui_ImplVulkan_InitInfo info{}; info.ApiVersion = VK_API_VERSION_1_2; info.Instance = m_instance; info.PhysicalDevice = m_physical; info.Device = m_device; info.QueueFamily = m_graphics_family; info.Queue = m_graphics_queue; info.DescriptorPool = m_imgui_pool; info.RenderPass = m_swapchain_rp; info.MinImageCount = 2; info.ImageCount = (uint32_t)m_images.size(); info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; if (!ImGui_ImplVulkan_Init(&info)) { DONUT_ERROR("Vulkan RHI: ImGui_ImplVulkan_Init failed"); return; } m_imgui = true; DONUT_INFO("Vulkan RHI: ImGui backend initialized"); } auto VulkanDevice::imgui_new_frame() -> void { if (!m_imgui) return; ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); } auto VulkanDevice::imgui_render(CommandList& cmds) -> void { if (!m_imgui) return; ImGui::Render(); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), static_cast(cmds).m_cmd); } auto VulkanDevice::shutdown() -> void { if (m_device == VK_NULL_HANDLE) { if (m_instance && m_surface) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); m_surface = VK_NULL_HANDLE; } if (m_instance) { vkDestroyInstance(m_instance, nullptr); m_instance = VK_NULL_HANDLE; } return; } vkDeviceWaitIdle(m_device); if (m_imgui) { ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); m_imgui = false; } if (m_imgui_pool) vkDestroyDescriptorPool(m_device, m_imgui_pool, nullptr); for (auto p : m_frame_pools) vkDestroyDescriptorPool(m_device, p, nullptr); m_frame_pools.clear(); for (auto s : m_render_finished) vkDestroySemaphore(m_device, s, nullptr); for (auto s : m_image_available) vkDestroySemaphore(m_device, s, nullptr); for (auto f : m_in_flight) vkDestroyFence(m_device, f, nullptr); m_render_finished.clear(); m_image_available.clear(); m_in_flight.clear(); if (m_command_pool) vkDestroyCommandPool(m_device, m_command_pool, nullptr); if (m_offscreen_rp) vkDestroyRenderPass(m_device, m_offscreen_rp, nullptr); if (m_swapchain_rp) vkDestroyRenderPass(m_device, m_swapchain_rp, nullptr); cleanup_swapchain(); vkDestroyDevice(m_device, nullptr); m_device = VK_NULL_HANDLE; if (m_surface) vkDestroySurfaceKHR(m_instance, m_surface, nullptr); if (m_instance) vkDestroyInstance(m_instance, nullptr); m_surface = VK_NULL_HANDLE; m_instance = VK_NULL_HANDLE; } } auto create_vulkan_device() -> Scope { return create_scope(); } auto vulkan_prepare_glfw() -> void { #ifdef __APPLE__ if (!getenv("VK_ICD_FILENAMES")) setenv("VK_ICD_FILENAMES", "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", 0); if (!getenv("VK_LAYER_PATH")) setenv("VK_LAYER_PATH", "/opt/homebrew/share/vulkan/explicit_layer.d", 0); if (!getenv("DYLD_LIBRARY_PATH")) setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); #endif glfwInitVulkanLoader(vkGetInstanceProcAddr); } }