From 5b361d81dbd2af0d0e99b9eb1adebea76ff05db0 Mon Sep 17 00:00:00 2001 From: hachem Date: Thu, 20 Aug 2026 02:27:02 +0200 Subject: [chore]: MASSIVE refactor + more vulkan bs --- src/platform/vulkan/vulkan_context.cpp | 789 ++++++++++++++++ src/platform/vulkan/vulkan_context.h | 41 + src/platform/vulkan/vulkan_index_buffer.cpp | 25 + src/platform/vulkan/vulkan_index_buffer.h | 22 + src/platform/vulkan/vulkan_renderer.cpp | 1235 +++++++++++++++++++++++++ src/platform/vulkan/vulkan_renderer.h | 40 + src/platform/vulkan/vulkan_renderer_api.cpp | 86 ++ src/platform/vulkan/vulkan_renderer_api.h | 38 + src/platform/vulkan/vulkan_shader.cpp | 146 +++ src/platform/vulkan/vulkan_shader.h | 52 ++ src/platform/vulkan/vulkan_texture.cpp | 73 ++ src/platform/vulkan/vulkan_texture.h | 60 ++ src/platform/vulkan/vulkan_uniform_buffer.cpp | 25 + src/platform/vulkan/vulkan_uniform_buffer.h | 19 + src/platform/vulkan/vulkan_vertex_array.cpp | 36 + src/platform/vulkan/vulkan_vertex_array.h | 28 + src/platform/vulkan/vulkan_vertex_buffer.cpp | 34 + src/platform/vulkan/vulkan_vertex_buffer.h | 26 + 18 files changed, 2775 insertions(+) create mode 100644 src/platform/vulkan/vulkan_context.cpp create mode 100644 src/platform/vulkan/vulkan_context.h create mode 100644 src/platform/vulkan/vulkan_index_buffer.cpp create mode 100644 src/platform/vulkan/vulkan_index_buffer.h create mode 100644 src/platform/vulkan/vulkan_renderer.cpp create mode 100644 src/platform/vulkan/vulkan_renderer.h create mode 100644 src/platform/vulkan/vulkan_renderer_api.cpp create mode 100644 src/platform/vulkan/vulkan_renderer_api.h create mode 100644 src/platform/vulkan/vulkan_shader.cpp create mode 100644 src/platform/vulkan/vulkan_shader.h create mode 100644 src/platform/vulkan/vulkan_texture.cpp create mode 100644 src/platform/vulkan/vulkan_texture.h create mode 100644 src/platform/vulkan/vulkan_uniform_buffer.cpp create mode 100644 src/platform/vulkan/vulkan_uniform_buffer.h create mode 100644 src/platform/vulkan/vulkan_vertex_array.cpp create mode 100644 src/platform/vulkan/vulkan_vertex_array.h create mode 100644 src/platform/vulkan/vulkan_vertex_buffer.cpp create mode 100644 src/platform/vulkan/vulkan_vertex_buffer.h (limited to 'src/platform/vulkan') diff --git a/src/platform/vulkan/vulkan_context.cpp b/src/platform/vulkan/vulkan_context.cpp new file mode 100644 index 0000000..ece7700 --- /dev/null +++ b/src/platform/vulkan/vulkan_context.cpp @@ -0,0 +1,789 @@ +#include "vulkan_context.h" +#include "core/log.h" + +#include + +#include +#include +#include "stb_image_write.h" + +#include +#include +#include +#include + +namespace Donut +{ + // Logs and returns false from the enclosing function on any non-success result. + #define VK_CHECK(expr) \ + do { \ + VkResult _r = (expr); \ + if (_r != VK_SUCCESS) { \ + DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \ + return false; \ + } \ + } while (0) + + struct VulkanContext::Impl + { + VkInstance instance = VK_NULL_HANDLE; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + VkQueue graphics_queue = VK_NULL_HANDLE; + uint32_t graphics_family = 0; + VkPhysicalDeviceMemoryProperties mem_props{}; + + auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t + { + for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) + if ((type_filter & (1u << i)) && + (mem_props.memoryTypes[i].propertyFlags & flags) == flags) + return i; + return UINT32_MAX; + } + + bool create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, + VkBuffer& buf, VkDeviceMemory& mem) const + { + VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bci.size = size; bci.usage = usage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &bci, nullptr, &buf) != VK_SUCCESS) return false; + VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(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(device, &ai, nullptr, &mem) != VK_SUCCESS) return false; + vkBindBufferMemory(device, buf, mem, 0); + return true; + } + }; + + VulkanContext::VulkanContext() { m_impl = new Impl(); } + VulkanContext::~VulkanContext() { shutdown(); delete m_impl; m_impl = nullptr; } + + auto VulkanContext::init() -> bool + { + Impl& v = *m_impl; + +#ifdef __APPLE__ + // The Homebrew Vulkan loader does not auto-discover MoltenVK or the + // validation layers; point it at both unless already configured. + 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); +#endif + + // Instance + VkApplicationInfo app{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; + app.pApplicationName = "Donut"; + app.apiVersion = VK_API_VERSION_1_2; + + // MoltenVK is a portability driver: without the portability-enumeration + // extension + flag, vkEnumeratePhysicalDevices returns zero devices. + std::vector exts = { + VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, + VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, + }; + + // Enable validation layers when they are installed (optional). + 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(); + VK_CHECK(vkCreateInstance(&ici, nullptr, &v.instance)); + + // Physical device + uint32_t device_count = 0; + vkEnumeratePhysicalDevices(v.instance, &device_count, nullptr); + if (device_count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; } + std::vector devices(device_count); + vkEnumeratePhysicalDevices(v.instance, &device_count, devices.data()); + v.physical = devices[0]; + + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(v.physical, &props); + vkGetPhysicalDeviceMemoryProperties(v.physical, &v.mem_props); + + // Graphics queue family + uint32_t q_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, nullptr); + std::vector qfams(q_count); + vkGetPhysicalDeviceQueueFamilyProperties(v.physical, &q_count, qfams.data()); + bool found = false; + for (uint32_t i = 0; i < q_count; ++i) + if (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { v.graphics_family = i; found = true; break; } + if (!found) { DONUT_ERROR("Vulkan: no graphics queue family"); return false; } + + // Logical device + // MoltenVK requires VK_KHR_portability_subset to be enabled if present. + std::vector dev_exts; + uint32_t dev_ext_count = 0; + vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, nullptr); + std::vector dev_ext_props(dev_ext_count); + vkEnumerateDeviceExtensionProperties(v.physical, nullptr, &dev_ext_count, dev_ext_props.data()); + for (const auto& e : dev_ext_props) + if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) + dev_exts.push_back("VK_KHR_portability_subset"); + + float priority = 1.0f; + VkDeviceQueueCreateInfo qci{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; + qci.queueFamilyIndex = v.graphics_family; + qci.queueCount = 1; + qci.pQueuePriorities = &priority; + + VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; + dci.queueCreateInfoCount = 1; + dci.pQueueCreateInfos = &qci; + dci.enabledExtensionCount = (uint32_t)dev_exts.size(); + dci.ppEnabledExtensionNames = dev_exts.data(); + VK_CHECK(vkCreateDevice(v.physical, &dci, nullptr, &v.device)); + vkGetDeviceQueue(v.device, v.graphics_family, 0, &v.graphics_queue); + + DONUT_INFO("Vulkan device: {} (API {}.{}.{}, validation {})", + props.deviceName, + VK_API_VERSION_MAJOR(props.apiVersion), + VK_API_VERSION_MINOR(props.apiVersion), + VK_API_VERSION_PATCH(props.apiVersion), + layers.empty() ? "off" : "on"); + return true; + } + + auto VulkanContext::self_test_clear() -> bool + { + Impl& v = *m_impl; + if (v.device == VK_NULL_HANDLE) return false; + + const uint32_t W = 64, H = 64; + const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; + + // Offscreen colour image + VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; + ici.format = fmt; + ici.extent = { W, 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_TRANSFER_SRC_BIT; + ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); + + VkMemoryRequirements im_req{}; + vkGetImageMemoryRequirements(v.device, image, &im_req); + VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + im_alloc.allocationSize = im_req.size; + im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); + VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); + + VkImageView view = VK_NULL_HANDLE; + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = image; + vci.viewType = VK_IMAGE_VIEW_TYPE_2D; + vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); + + // Render pass (clear -> store, leave in TRANSFER_SRC for readback) + VkAttachmentDescription color{}; + color.format = 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_TRANSFER_SRC_OPTIMAL; + + VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_ref; + + // Ensure colour writes finish before the read-back copy. + 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_TRANSFER_BIT; + dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + + VkRenderPass render_pass = VK_NULL_HANDLE; + 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; + VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); + + VkFramebuffer fb = VK_NULL_HANDLE; + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = render_pass; + fbci.attachmentCount = 1; fbci.pAttachments = &view; + fbci.width = W; fbci.height = H; fbci.layers = 1; + VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); + + // Host-visible staging buffer for read-back + VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; + VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bci.size = (VkDeviceSize)W * H * 4; + bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging)); + VkMemoryRequirements b_req{}; + vkGetBufferMemoryRequirements(v.device, staging, &b_req); + VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + b_alloc.allocationSize = b_req.size; + b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem)); + VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0)); + + // Command buffer: clear via render pass, then copy image -> buffer + VkCommandPool pool = VK_NULL_HANDLE; + VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; + pci.queueFamilyIndex = v.graphics_family; + VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool)); + + VkCommandBuffer cmd = VK_NULL_HANDLE; + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); + + VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); + + VkClearValue clear{}; + clear.color = { { 0.2f, 0.4f, 0.8f, 1.0f } }; // -> RGBA8 (51, 102, 204, 255) + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rpbi.renderPass = render_pass; rpbi.framebuffer = fb; + rpbi.renderArea = { { 0, 0 }, { W, H } }; + rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; + vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(cmd); + + VkBufferImageCopy region{}; + region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; + region.imageExtent = { W, H, 1 }; + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, ®ion); + VK_CHECK(vkEndCommandBuffer(cmd)); + + VkFence fence = VK_NULL_HANDLE; + VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; + VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; + submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; + VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); + VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); + + // Read back + verify + void* mapped = nullptr; + VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped)); + const uint8_t* px = (const uint8_t*)mapped; + DONUT_INFO("Vulkan clear self-test: pixel RGBA = ({}, {}, {}, {})", + (int)px[0], (int)px[1], (int)px[2], (int)px[3]); + bool ok = px[0] > 45 && px[0] < 60 && px[1] > 95 && px[1] < 110 && + px[2] > 195 && px[2] < 210 && px[3] == 255; + vkUnmapMemory(v.device, staging_mem); + DONUT_INFO("Vulkan clear self-test: {}", ok ? "PASS" : "FAIL"); + + // Cleanup + vkDestroyFence(v.device, fence, nullptr); + vkDestroyCommandPool(v.device, pool, nullptr); + vkDestroyBuffer(v.device, staging, nullptr); + vkFreeMemory(v.device, staging_mem, nullptr); + vkDestroyFramebuffer(v.device, fb, nullptr); + vkDestroyRenderPass(v.device, render_pass, nullptr); + vkDestroyImageView(v.device, view, nullptr); + vkDestroyImage(v.device, image, nullptr); + vkFreeMemory(v.device, image_mem, nullptr); + return ok; + } + + static std::vector load_spirv(const std::string& path) + { + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) return {}; + size_t size = (size_t)f.tellg(); + std::vector data(size / 4); + f.seekg(0); + f.read((char*)data.data(), (std::streamsize)size); + return data; + } + + auto VulkanContext::self_test_triangle() -> bool + { + Impl& v = *m_impl; + if (v.device == VK_NULL_HANDLE) return false; + + const uint32_t W = 64, H = 64; + const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; + + // Offscreen image + view (as in the clear test) + VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, 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_TRANSFER_SRC_BIT; + VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); + VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req); + VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + im_alloc.allocationSize = im_req.size; + im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); + VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); + VkImageView view = VK_NULL_HANDLE; + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); + + // Render pass + framebuffer + VkAttachmentDescription color{}; + color.format = 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_TRANSFER_SRC_OPTIMAL; + VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_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_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + VkRenderPass render_pass = VK_NULL_HANDLE; + 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; + VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); + VkFramebuffer fb = VK_NULL_HANDLE; + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view; + fbci.width = W; fbci.height = H; fbci.layers = 1; + VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); + + // Shader modules from Slang SPIR-V + auto vspv = load_spirv("assets/shaders/generated/VkPipelineTest.vertexMain.spv"); + auto fspv = load_spirv("assets/shaders/generated/VkPipelineTest.fragmentMain.spv"); + if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: VkPipelineTest SPIR-V not found"); return false; } + VkShaderModule vmod = VK_NULL_HANDLE, fmod = VK_NULL_HANDLE; + VkShaderModuleCreateInfo smci{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; + smci.codeSize = vspv.size() * 4; smci.pCode = vspv.data(); + VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &vmod)); + smci.codeSize = fspv.size() * 4; smci.pCode = fspv.data(); + VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &fmod)); + + // Graphics pipeline + 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"; + + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkViewport vp{ 0, 0, (float)W, (float)H, 0, 1 }; + VkRect2D scissor{ { 0, 0 }, { W, H } }; + VkPipelineViewportStateCreateInfo vps{ VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO }; + vps.viewportCount = 1; vps.pViewports = &vp; vps.scissorCount = 1; vps.pScissors = &scissor; + 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; + + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout)); + + VkPipeline pipeline = VK_NULL_HANDLE; + 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 = layout; gpci.renderPass = render_pass; gpci.subpass = 0; + VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)); + + // Readback staging buffer + VkBuffer staging = VK_NULL_HANDLE; VkDeviceMemory staging_mem = VK_NULL_HANDLE; + VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO }; + bci.size = (VkDeviceSize)W * H * 4; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + VK_CHECK(vkCreateBuffer(v.device, &bci, nullptr, &staging)); + VkMemoryRequirements b_req{}; vkGetBufferMemoryRequirements(v.device, staging, &b_req); + VkMemoryAllocateInfo b_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + b_alloc.allocationSize = b_req.size; + b_alloc.memoryTypeIndex = v.find_memory_type(b_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + VK_CHECK(vkAllocateMemory(v.device, &b_alloc, nullptr, &staging_mem)); + VK_CHECK(vkBindBufferMemory(v.device, staging, staging_mem, 0)); + + // Record + submit + VkCommandPool pool = VK_NULL_HANDLE; + VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; + pci.queueFamilyIndex = v.graphics_family; + VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &pool)); + VkCommandBuffer cmd = VK_NULL_HANDLE; + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); + VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); + VkClearValue clear{}; clear.color = { { 0.0f, 0.0f, 0.0f, 1.0f } }; + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rpbi.renderPass = render_pass; rpbi.framebuffer = fb; + rpbi.renderArea = { { 0, 0 }, { W, H } }; + rpbi.clearValueCount = 1; rpbi.pClearValues = &clear; + vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdDraw(cmd, 3, 1, 0, 0); + vkCmdEndRenderPass(cmd); + VkBufferImageCopy region{}; + region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; + region.imageExtent = { W, H, 1 }; + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging, 1, ®ion); + VK_CHECK(vkEndCommandBuffer(cmd)); + + VkFence fence = VK_NULL_HANDLE; + VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; + VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; + submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; + VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); + VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); + + // Verify: centre pixel should be the mid-gradient (not black) + void* mapped = nullptr; + VK_CHECK(vkMapMemory(v.device, staging_mem, 0, bci.size, 0, &mapped)); + const uint8_t* px = (const uint8_t*)mapped; + size_t c = ((size_t)(H / 2) * W + (W / 2)) * 4; + DONUT_INFO("Vulkan triangle self-test: centre pixel RGBA = ({}, {}, {}, {})", + (int)px[c + 0], (int)px[c + 1], (int)px[c + 2], (int)px[c + 3]); + bool ok = (px[c + 0] > 40 || px[c + 1] > 40) && px[c + 3] == 255; + vkUnmapMemory(v.device, staging_mem); + DONUT_INFO("Vulkan triangle self-test: {}", ok ? "PASS" : "FAIL"); + + // Cleanup + vkDestroyFence(v.device, fence, nullptr); + vkDestroyCommandPool(v.device, pool, nullptr); + vkDestroyBuffer(v.device, staging, nullptr); + vkFreeMemory(v.device, staging_mem, nullptr); + vkDestroyPipeline(v.device, pipeline, nullptr); + vkDestroyPipelineLayout(v.device, layout, nullptr); + vkDestroyShaderModule(v.device, vmod, nullptr); + vkDestroyShaderModule(v.device, fmod, nullptr); + vkDestroyFramebuffer(v.device, fb, nullptr); + vkDestroyRenderPass(v.device, render_pass, nullptr); + vkDestroyImageView(v.device, view, nullptr); + vkDestroyImage(v.device, image, nullptr); + vkFreeMemory(v.device, image_mem, nullptr); + return ok; + } + + auto VulkanContext::render_geodesic(const char* png_path) -> bool + { + Impl& v = *m_impl; + if (v.device == VK_NULL_HANDLE) return false; + + const uint32_t W = 384, H = 216; + const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; + const float SagA_rs = 1.269e10f; + + // Offscreen colour target + VkImage image = VK_NULL_HANDLE; VkDeviceMemory image_mem = VK_NULL_HANDLE; + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { W, 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_TRANSFER_SRC_BIT; + VK_CHECK(vkCreateImage(v.device, &ici, nullptr, &image)); + VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(v.device, image, &im_req); + VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + im_alloc.allocationSize = im_req.size; + im_alloc.memoryTypeIndex = v.find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vkAllocateMemory(v.device, &im_alloc, nullptr, &image_mem)); + VK_CHECK(vkBindImageMemory(v.device, image, image_mem, 0)); + VkImageView view = VK_NULL_HANDLE; + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VK_CHECK(vkCreateImageView(v.device, &vci, nullptr, &view)); + + VkAttachmentDescription color{}; + color.format = 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_TRANSFER_SRC_OPTIMAL; + VkAttachmentReference color_ref{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color_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_TRANSFER_BIT; dep.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + VkRenderPass render_pass = VK_NULL_HANDLE; + 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; + VK_CHECK(vkCreateRenderPass(v.device, &rpci, nullptr, &render_pass)); + VkFramebuffer fb = VK_NULL_HANDLE; + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &view; + fbci.width = W; fbci.height = H; fbci.layers = 1; + VK_CHECK(vkCreateFramebuffer(v.device, &fbci, nullptr, &fb)); + + // Uniform buffers (host-visible), filled to match the shader's std140 layout + const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + VkBuffer cam_buf, disk_buf, obj_buf, sim_buf; + VkDeviceMemory cam_mem, disk_mem, obj_mem, sim_mem; + v.create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem); + v.create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem); + v.create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem); + v.create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem); + + struct CamUBO { + glm::vec3 pos; float p0; glm::vec3 right; float p1; + glm::vec3 up; float p2; glm::vec3 fwd; float p3; + float tan_half_fov; float aspect; uint32_t moving; int p4; + } cam{}; + glm::vec3 cam_pos(1e11f, 0.32e11f, 0.0f); + glm::vec3 fwd = glm::normalize(glm::vec3(0.0f) - cam_pos); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 up = glm::cross(right, fwd); + cam.pos = cam_pos; cam.right = right; cam.up = up; cam.fwd = fwd; + cam.tan_half_fov = 0.57735f; cam.aspect = (float)W / (float)H; cam.moving = 0; + void* p = nullptr; + vkMapMemory(v.device, cam_mem, 0, 128, 0, &p); memcpy(p, &cam, sizeof(cam)); vkUnmapMemory(v.device, cam_mem); + + float disk[8] = { SagA_rs * 2.2f, SagA_rs * 5.2f, 2.0f, SagA_rs * 0.1f, 0.1f, 0, 0, 0 }; + vkMapMemory(v.device, disk_mem, 0, 32, 0, &p); memcpy(p, disk, sizeof(disk)); vkUnmapMemory(v.device, disk_mem); + + std::vector obj_data(800, 0); + int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4); + float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16); + float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16); + vkMapMemory(v.device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(v.device, obj_mem); + + struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim{ 6000, 6000, 5e12f, 0.0f }; + vkMapMemory(v.device, sim_mem, 0, 16, 0, &p); memcpy(p, &sim, sizeof(sim)); vkUnmapMemory(v.device, sim_mem); + + // Dark cubemap (stands in for the HDRI for now) + VkImage cube = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE; + VkImageCreateInfo cci{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + cci.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + cci.imageType = VK_IMAGE_TYPE_2D; cci.format = fmt; cci.extent = { 1, 1, 1 }; + cci.mipLevels = 1; cci.arrayLayers = 6; cci.samples = VK_SAMPLE_COUNT_1_BIT; + cci.tiling = VK_IMAGE_TILING_OPTIMAL; + cci.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + VK_CHECK(vkCreateImage(v.device, &cci, nullptr, &cube)); + VkMemoryRequirements cube_req{}; vkGetImageMemoryRequirements(v.device, cube, &cube_req); + VkMemoryAllocateInfo cube_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + cube_alloc.allocationSize = cube_req.size; + cube_alloc.memoryTypeIndex = v.find_memory_type(cube_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vkAllocateMemory(v.device, &cube_alloc, nullptr, &cube_mem)); + VK_CHECK(vkBindImageMemory(v.device, cube, cube_mem, 0)); + VkImageView cube_view = VK_NULL_HANDLE; + VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + cvci.image = cube; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = fmt; + cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; + VK_CHECK(vkCreateImageView(v.device, &cvci, nullptr, &cube_view)); + VkSampler sampler = VK_NULL_HANDLE; + VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR; + smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VK_CHECK(vkCreateSampler(v.device, &smci, nullptr, &sampler)); + + VkBuffer cube_staging; VkDeviceMemory cube_staging_mem; + v.create_buffer(6 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, cube_staging, cube_staging_mem); + uint8_t cube_pixels[6 * 4]; + for (int i = 0; i < 6; ++i) { cube_pixels[i * 4 + 0] = 6; cube_pixels[i * 4 + 1] = 6; cube_pixels[i * 4 + 2] = 14; cube_pixels[i * 4 + 3] = 255; } + vkMapMemory(v.device, cube_staging_mem, 0, 24, 0, &p); memcpy(p, cube_pixels, 24); vkUnmapMemory(v.device, cube_staging_mem); + + // Descriptor set: 4 UBOs (bindings 0-3) + cubemap sampler (binding 4) + VkDescriptorSetLayoutBinding binds[5]{}; + for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } + binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayout set_layout = VK_NULL_HANDLE; + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + dslci.bindingCount = 5; dslci.pBindings = binds; + VK_CHECK(vkCreateDescriptorSetLayout(v.device, &dslci, nullptr, &set_layout)); + VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; + VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &pool)); + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; + VK_CHECK(vkAllocateDescriptorSets(v.device, &dsai, &set)); + + // load geodesic SPIR-V + build the pipeline + auto vspv = load_spirv("assets/shaders/generated/Geodesic.vertexMain.spv"); + auto fspv = load_spirv("assets/shaders/generated/Geodesic.fragmentMain.spv"); + if (vspv.empty() || fspv.empty()) { DONUT_ERROR("Vulkan: geodesic SPIR-V not found"); return false; } + VkShaderModule vmod, fmod; + VkShaderModuleCreateInfo smci2{ VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO }; + smci2.codeSize = vspv.size() * 4; smci2.pCode = vspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &vmod)); + smci2.codeSize = fspv.size() * 4; smci2.pCode = fspv.data(); VK_CHECK(vkCreateShaderModule(v.device, &smci2, nullptr, &fmod)); + + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; + VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &layout)); + + 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, 16, VK_VERTEX_INPUT_RATE_VERTEX }; + VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; + vin.vertexAttributeDescriptionCount = 2; 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)W, (float)H, 0, 1 }; VkRect2D sc{ { 0, 0 }, { W, H } }; + 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 = VK_NULL_HANDLE; + 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 = layout; gpci.renderPass = render_pass; gpci.subpass = 0; + VK_CHECK(vkCreateGraphicsPipelines(v.device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)); + + // Fullscreen quad (position.xy, texcoord.uv) + float quad[] = { + -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, + -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, + }; + VkBuffer vbuf; VkDeviceMemory vbuf_mem; + v.create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, vbuf, vbuf_mem); + vkMapMemory(v.device, vbuf_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(v.device, vbuf_mem); + + // Write the descriptor set + VkDescriptorBufferInfo bi[4] = { + { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } }; + VkDescriptorImageInfo ii{ sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet writes[5]{}; + for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; } + writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = ⅈ + vkUpdateDescriptorSets(v.device, 5, writes, 0, nullptr); + + VkBuffer readback; VkDeviceMemory readback_mem; + v.create_buffer((VkDeviceSize)W * H * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, host_vis, readback, readback_mem); + + // Record + submit + VkCommandPool cpool = VK_NULL_HANDLE; + VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; pci.queueFamilyIndex = v.graphics_family; + VK_CHECK(vkCreateCommandPool(v.device, &pci, nullptr, &cpool)); + VkCommandBuffer cmd = VK_NULL_HANDLE; + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; cbai.commandPool = cpool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cmd)); + VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); + + 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 = cube; to_dst.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; + 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 cube_copy{}; cube_copy.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 6 }; cube_copy.imageExtent = { 1, 1, 1 }; + vkCmdCopyBufferToImage(cmd, cube_staging, cube, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cube_copy); + 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 } }; + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rpbi.renderPass = render_pass; rpbi.framebuffer = fb; rpbi.renderArea = { { 0, 0 }, { W, H } }; + 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, layout, 0, 1, &set, 0, nullptr); + VkDeviceSize voff = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &vbuf, &voff); + vkCmdDraw(cmd, 6, 1, 0, 0); + vkCmdEndRenderPass(cmd); + VkBufferImageCopy region{}; region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; region.imageExtent = { W, H, 1 }; + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback, 1, ®ion); + VK_CHECK(vkEndCommandBuffer(cmd)); + + VkFence fence = VK_NULL_HANDLE; VkFenceCreateInfo fci{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; + VK_CHECK(vkCreateFence(v.device, &fci, nullptr, &fence)); + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; submit.commandBufferCount = 1; submit.pCommandBuffers = &cmd; + VK_CHECK(vkQueueSubmit(v.graphics_queue, 1, &submit, fence)); + VK_CHECK(vkWaitForFences(v.device, 1, &fence, VK_TRUE, UINT64_MAX)); + + vkMapMemory(v.device, readback_mem, 0, (VkDeviceSize)W * H * 4, 0, &p); + stbi_write_png(png_path, W, H, 4, p, W * 4); + vkUnmapMemory(v.device, readback_mem); + DONUT_INFO("Vulkan geodesic render written to {}", png_path); + + vkDestroyFence(v.device, fence, nullptr); + vkDestroyCommandPool(v.device, cpool, nullptr); + vkDestroyBuffer(v.device, readback, nullptr); vkFreeMemory(v.device, readback_mem, nullptr); + vkDestroyBuffer(v.device, vbuf, nullptr); vkFreeMemory(v.device, vbuf_mem, nullptr); + vkDestroyPipeline(v.device, pipeline, nullptr); vkDestroyPipelineLayout(v.device, layout, nullptr); + vkDestroyShaderModule(v.device, vmod, nullptr); vkDestroyShaderModule(v.device, fmod, nullptr); + vkDestroyDescriptorPool(v.device, pool, nullptr); vkDestroyDescriptorSetLayout(v.device, set_layout, nullptr); + vkDestroySampler(v.device, sampler, nullptr); vkDestroyImageView(v.device, cube_view, nullptr); + vkDestroyImage(v.device, cube, nullptr); vkFreeMemory(v.device, cube_mem, nullptr); + vkDestroyBuffer(v.device, cube_staging, nullptr); vkFreeMemory(v.device, cube_staging_mem, nullptr); + vkDestroyBuffer(v.device, cam_buf, nullptr); vkFreeMemory(v.device, cam_mem, nullptr); + vkDestroyBuffer(v.device, disk_buf, nullptr); vkFreeMemory(v.device, disk_mem, nullptr); + vkDestroyBuffer(v.device, obj_buf, nullptr); vkFreeMemory(v.device, obj_mem, nullptr); + vkDestroyBuffer(v.device, sim_buf, nullptr); vkFreeMemory(v.device, sim_mem, nullptr); + vkDestroyFramebuffer(v.device, fb, nullptr); vkDestroyRenderPass(v.device, render_pass, nullptr); + vkDestroyImageView(v.device, view, nullptr); vkDestroyImage(v.device, image, nullptr); vkFreeMemory(v.device, image_mem, nullptr); + return true; + } + + auto VulkanContext::shutdown() -> void + { + Impl& v = *m_impl; + if (v.device) { vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; } + if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } + } + + auto vulkan_self_test() -> bool + { + VulkanContext ctx; + if (!ctx.init()) + { + DONUT_ERROR("Vulkan: initialization failed"); + return false; + } + bool ok = ctx.self_test_clear(); + ok = ctx.self_test_triangle() && ok; + ctx.shutdown(); + return ok; + } +} diff --git a/src/platform/vulkan/vulkan_context.h b/src/platform/vulkan/vulkan_context.h new file mode 100644 index 0000000..de57c90 --- /dev/null +++ b/src/platform/vulkan/vulkan_context.h @@ -0,0 +1,41 @@ +#pragma once + +// Pure-C++ interface to the Vulkan backend (no vulkan.h leaks into the rest of +// the engine; the implementation lives in VulkanContext.cpp). On macOS Vulkan +// runs through MoltenVK (Vulkan -> Metal). +namespace Donut +{ + class VulkanContext + { + public: + VulkanContext(); + ~VulkanContext(); + + // Creates the instance, picks a physical device, and creates the logical + // device + graphics queue. Returns false (and logs) on failure. + auto init() -> bool; + auto shutdown() -> void; + + // Phase 1 verification: renders a known clear colour into an offscreen + // image and reads it back, confirming instance -> device -> render pass + // -> command buffer -> submit -> read-back all work end to end. + auto self_test_clear() -> bool; + + // Phase 2/3 verification: builds a graphics pipeline from Slang-compiled + // SPIR-V and draws a full-screen gradient triangle into the offscreen + // image, confirming the SPIR-V -> pipeline -> draw path works. + auto self_test_triangle() -> bool; + + // B-3: renders the geodesic (black hole) fragment shader through Vulkan + // into an offscreen image and writes it to pngPath. Exercises UBOs, + // descriptor sets, a cubemap sampler and the geodesic pipeline. + auto render_geodesic(const char* pngPath) -> bool; + + private: + struct Impl; + Impl* m_impl = nullptr; + }; + + // Convenience one-shot: init() + self_test_clear() + shutdown(). Logs results. + auto vulkan_self_test() -> bool; +} diff --git a/src/platform/vulkan/vulkan_index_buffer.cpp b/src/platform/vulkan/vulkan_index_buffer.cpp new file mode 100644 index 0000000..a6e5912 --- /dev/null +++ b/src/platform/vulkan/vulkan_index_buffer.cpp @@ -0,0 +1,25 @@ +#include "vulkan_index_buffer.h" + +namespace Donut +{ + VulkanIndexBuffer::VulkanIndexBuffer(uint32_t* indices, uint32_t count) + : m_count(count) + { + // TODO(Hachem): Implement Vulkan index buffer creation + } + + VulkanIndexBuffer::~VulkanIndexBuffer() + { + // TODO(Hachem): Implement Vulkan index buffer cleanup + } + + auto VulkanIndexBuffer::bind() const -> void + { + // TODO(Hachem): Implement Vulkan index buffer binding + } + + auto VulkanIndexBuffer::unbind() const -> void + { + // TODO(Hachem): Implement Vulkan index buffer unbinding + } +}; diff --git a/src/platform/vulkan/vulkan_index_buffer.h b/src/platform/vulkan/vulkan_index_buffer.h new file mode 100644 index 0000000..a0734a8 --- /dev/null +++ b/src/platform/vulkan/vulkan_index_buffer.h @@ -0,0 +1,22 @@ +#pragma once + +#include "rendering/index_buffer.h" + +namespace Donut +{ + class VulkanIndexBuffer + : public IndexBuffer + { + public: + VulkanIndexBuffer(uint32_t* indices, uint32_t count); + virtual ~VulkanIndexBuffer(); + + virtual auto bind() const -> void override; + virtual auto unbind() const -> void override; + + virtual auto get_count() const -> uint32_t override{ return m_count; } + private: + uint32_t m_renderer_id; + uint32_t m_count; + }; +}; diff --git a/src/platform/vulkan/vulkan_renderer.cpp b/src/platform/vulkan/vulkan_renderer.cpp new file mode 100644 index 0000000..95a546b --- /dev/null +++ b/src/platform/vulkan/vulkan_renderer.cpp @@ -0,0 +1,1235 @@ +#include "vulkan_renderer.h" +#include "core/log.h" +#include "core/camera.h" + +#define GLFW_INCLUDE_VULKAN +#include + +#include +#include +#include + +#include +#include +#include "stb_image.h" + +#include +#include +#include +#include +#include + +namespace Donut +{ + #define VK_CHECK(expr) \ + do { \ + VkResult _r = (expr); \ + if (_r != VK_SUCCESS) { \ + DONUT_ERROR("Vulkan: {} failed ({})", #expr, (int)_r); \ + return false; \ + } \ + } while (0) + + static constexpr int MAX_FRAMES_IN_FLIGHT = 2; + + 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 + // GLFW dlopen's the Vulkan loader by bare name, which fails on + // mac_os/Homebrew; hand it the loader entry point we already link against. + glfwInitVulkanLoader(vkGetInstanceProcAddr); + } + + struct VulkanRenderer::Impl + { + GLFWwindow* window = nullptr; + int width = 0, height = 0; + bool framebuffer_resized = false; + + VkInstance instance = VK_NULL_HANDLE; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + uint32_t graphics_family = 0, present_family = 0; + VkQueue graphics_queue = VK_NULL_HANDLE, present_queue = VK_NULL_HANDLE; + + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkFormat swapchain_format = VK_FORMAT_B8G8R8A8_UNORM; + VkExtent2D swapchain_extent{}; + std::vector images; + std::vector image_views; + VkRenderPass render_pass = VK_NULL_HANDLE; + std::vector framebuffers; + + VkCommandPool command_pool = VK_NULL_HANDLE; + std::vector command_buffers; // MAX_FRAMES_IN_FLIGHT + + std::vector image_available; // per frame in flight + std::vector render_finished; // per swapchain image + std::vector in_flight; // per frame in flight + std::vector images_in_flight; // per swapchain image + uint32_t current_frame = 0; + + VkDescriptorPool imgui_pool = VK_NULL_HANDLE; + bool imgui_init = false; + + VkPhysicalDeviceMemoryProperties mem_props{}; + + // Geodesic scene, rendered every frame into a fixed low-resolution + // offscreen image (keeps each draw well under the Metal GPU watchdog), + // then upscaled onto the swapchain by the present pass below. + static constexpr uint32_t GEO_W = 480, GEO_H = 270; + VkImage geo_image = VK_NULL_HANDLE; + VkDeviceMemory geo_image_mem = VK_NULL_HANDLE; + VkImageView geo_image_view = VK_NULL_HANDLE; + VkRenderPass geo_render_pass = VK_NULL_HANDLE; + VkFramebuffer geo_framebuffer = VK_NULL_HANDLE; + VkBuffer cam_buf = VK_NULL_HANDLE, disk_buf = VK_NULL_HANDLE, obj_buf = VK_NULL_HANDLE, sim_buf = VK_NULL_HANDLE; + VkDeviceMemory cam_mem = VK_NULL_HANDLE, disk_mem = VK_NULL_HANDLE, obj_mem = VK_NULL_HANDLE, sim_mem = VK_NULL_HANDLE; + void* cam_mapped = nullptr; + void* sim_mapped = nullptr; + Camera camera{ 60.0f, (float)GEO_W / (float)GEO_H, 0.1f, 100.0f }; + bool left_was_down = false; + VkImage cube_image = VK_NULL_HANDLE; VkDeviceMemory cube_mem = VK_NULL_HANDLE; + VkImageView cube_view = VK_NULL_HANDLE; VkSampler cube_sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout geo_set_layout = VK_NULL_HANDLE; + VkDescriptorPool geo_pool = VK_NULL_HANDLE; + VkDescriptorSet geo_set = VK_NULL_HANDLE; + VkPipelineLayout geo_pipeline_layout = VK_NULL_HANDLE; + VkPipeline geo_pipeline = VK_NULL_HANDLE; + VkBuffer quad_vb = VK_NULL_HANDLE; VkDeviceMemory quad_vb_mem = VK_NULL_HANDLE; + double start_time = 0.0; + VkFence geo_in_use = VK_NULL_HANDLE; // previous frame's fence; guards the shared geodesic image + + // Present pass: samples the geodesic image with a full-screen textured + // quad, drawn into the swapchain render pass just before the ImGui UI. + VkSampler present_sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout present_set_layout = VK_NULL_HANDLE; + VkDescriptorPool present_pool = VK_NULL_HANDLE; + VkDescriptorSet present_set = VK_NULL_HANDLE; + VkPipelineLayout present_pipeline_layout = VK_NULL_HANDLE; + VkPipeline present_pipeline = VK_NULL_HANDLE; + + auto find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t; + auto create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, VkBuffer& buf, VkDeviceMemory& mem) const -> bool; + static auto load_spirv(const std::string& path) -> 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_render_pass() -> bool; + auto create_framebuffers() -> bool; + auto create_command_buffers() -> bool; + auto create_sync_objects() -> bool; + auto create_geodesic_resources() -> bool; + auto create_hdri_cubemap(const char* path) -> bool; + auto create_present_resources() -> bool; + auto process_input() -> void; + auto update_geodesic_uniforms() -> void; + auto destroy_geodesic_resources() -> void; + auto recreate_swapchain() -> bool; + auto cleanup_swapchain() -> void; + auto record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool; + }; + + auto VulkanRenderer::Impl::create_instance() -> bool + { +#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); + // The Homebrew validation-layer manifest names the dylib without a path; + // let dlopen find it in the Homebrew lib dir. + if (!getenv("DYLD_LIBRARY_PATH")) + setenv("DYLD_LIBRARY_PATH", "/opt/homebrew/lib", 0); +#endif + 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: GLFW reports no surface support"); return false; } + std::vector exts; + for (uint32_t i = 0; i < glfwExtCount; ++i) exts.push_back(glfwExts[i]); + 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, &instance); + if (r != VK_SUCCESS && !layers.empty()) + { + // The validation layer failed to load (its dylib isn't on the loader + // search path); it is optional, so retry without it. + DONUT_WARN("Vulkan: validation layer unavailable, continuing without it"); + layers.clear(); + ici.enabledLayerCount = 0; + ici.ppEnabledLayerNames = nullptr; + r = vkCreateInstance(&ici, nullptr, &instance); + } + if (r != VK_SUCCESS) { DONUT_ERROR("Vulkan: vkCreateInstance failed ({})", (int)r); return false; } + + VK_CHECK(glfwCreateWindowSurface(instance, window, nullptr, &surface)); + DONUT_INFO("Vulkan: instance + surface created (validation {})", layers.empty() ? "off" : "on"); + return true; + } + + auto VulkanRenderer::Impl::pick_physical_and_device() -> bool + { + uint32_t count = 0; + vkEnumeratePhysicalDevices(instance, &count, nullptr); + if (count == 0) { DONUT_ERROR("Vulkan: no physical devices"); return false; } + std::vector devices(count); + vkEnumeratePhysicalDevices(instance, &count, devices.data()); + physical = devices[0]; + + uint32_t q_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, nullptr); + std::vector qfams(q_count); + vkGetPhysicalDeviceQueueFamilyProperties(physical, &q_count, qfams.data()); + bool found_g = false, found_p = false; + for (uint32_t i = 0; i < q_count; ++i) + { + if (!found_g && (qfams[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)) { graphics_family = i; found_g = true; } + VkBool32 present = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(physical, i, surface, &present); + if (!found_p && present) { present_family = i; found_p = true; } + } + if (!found_g || !found_p) { DONUT_ERROR("Vulkan: no graphics/present queue"); return false; } + + std::vector dev_exts = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + uint32_t dev_ext_count = 0; + vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, nullptr); + std::vector dev_ext_props(dev_ext_count); + vkEnumerateDeviceExtensionProperties(physical, nullptr, &dev_ext_count, dev_ext_props.data()); + for (const auto& e : dev_ext_props) + 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] = { graphics_family, present_family }; + for (uint32_t i = 0; i < (graphics_family == 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(); + VK_CHECK(vkCreateDevice(physical, &dci, nullptr, &device)); + vkGetDeviceQueue(device, graphics_family, 0, &graphics_queue); + vkGetDeviceQueue(device, present_family, 0, &present_queue); + + VkPhysicalDeviceProperties props{}; + vkGetPhysicalDeviceProperties(physical, &props); + vkGetPhysicalDeviceMemoryProperties(physical, &mem_props); + DONUT_INFO("Vulkan device: {}", props.deviceName); + return true; + } + + auto VulkanRenderer::Impl::create_swapchain() -> bool + { + VkSurfaceCapabilitiesKHR caps{}; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, surface, &caps); + + uint32_t fmt_count = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, nullptr); + std::vector formats(fmt_count); + vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &fmt_count, 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; + swapchain_format = chosen.format; + + if (caps.currentExtent.width != UINT32_MAX) + swapchain_extent = caps.currentExtent; + else + { + swapchain_extent.width = std::clamp((uint32_t)width, caps.minImageExtent.width, caps.maxImageExtent.width); + swapchain_extent.height = std::clamp((uint32_t)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 = surface; + sci.minImageCount = image_count; + sci.imageFormat = chosen.format; + sci.imageColorSpace = chosen.colorSpace; + sci.imageExtent = swapchain_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; // always supported, vsync + sci.clipped = VK_TRUE; + + uint32_t fam_idx[2] = { graphics_family, present_family }; + if (graphics_family != present_family) + { + sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + sci.queueFamilyIndexCount = 2; + sci.pQueueFamilyIndices = fam_idx; + } + else + sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VK_CHECK(vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain)); + uint32_t n = 0; + vkGetSwapchainImagesKHR(device, swapchain, &n, nullptr); + images.resize(n); + vkGetSwapchainImagesKHR(device, swapchain, &n, images.data()); + return true; + } + + auto VulkanRenderer::Impl::create_image_views() -> bool + { + image_views.resize(images.size()); + for (size_t i = 0; i < images.size(); ++i) + { + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = images[i]; + vci.viewType = VK_IMAGE_VIEW_TYPE_2D; + vci.format = swapchain_format; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VK_CHECK(vkCreateImageView(device, &vci, nullptr, &image_views[i])); + } + return true; + } + + auto VulkanRenderer::Impl::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 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 = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + 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; + VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &render_pass)); + return true; + } + + auto VulkanRenderer::Impl::create_framebuffers() -> bool + { + framebuffers.resize(image_views.size()); + for (size_t i = 0; i < image_views.size(); ++i) + { + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = render_pass; + fbci.attachmentCount = 1; fbci.pAttachments = &image_views[i]; + fbci.width = swapchain_extent.width; fbci.height = swapchain_extent.height; fbci.layers = 1; + VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &framebuffers[i])); + } + return true; + } + + auto VulkanRenderer::Impl::create_command_buffers() -> bool + { + VkCommandPoolCreateInfo pci{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; + pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pci.queueFamilyIndex = graphics_family; + VK_CHECK(vkCreateCommandPool(device, &pci, nullptr, &command_pool)); + + command_buffers.resize(MAX_FRAMES_IN_FLIGHT); + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = command_pool; + cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cbai.commandBufferCount = MAX_FRAMES_IN_FLIGHT; + VK_CHECK(vkAllocateCommandBuffers(device, &cbai, command_buffers.data())); + return true; + } + + auto VulkanRenderer::Impl::create_sync_objects() -> bool + { + image_available.resize(MAX_FRAMES_IN_FLIGHT); + in_flight.resize(MAX_FRAMES_IN_FLIGHT); + render_finished.resize(images.size()); + images_in_flight.assign(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) + { + VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &image_available[i])); + VK_CHECK(vkCreateFence(device, &fci, nullptr, &in_flight[i])); + } + for (size_t i = 0; i < images.size(); ++i) + VK_CHECK(vkCreateSemaphore(device, &sci, nullptr, &render_finished[i])); + return true; + } + + auto VulkanRenderer::Impl::find_memory_type(uint32_t type_filter, VkMemoryPropertyFlags flags) const -> uint32_t + { + for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) + if ((type_filter & (1u << i)) && (mem_props.memoryTypes[i].propertyFlags & flags) == flags) + return i; + return UINT32_MAX; + } + + auto VulkanRenderer::Impl::create_buffer(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(device, &bci, nullptr, &buf) != VK_SUCCESS) return false; + VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(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(device, &ai, nullptr, &mem) != VK_SUCCESS) return false; + vkBindBufferMemory(device, buf, mem, 0); + return true; + } + + auto VulkanRenderer::Impl::load_spirv(const std::string& path) -> 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 VulkanRenderer::Impl::create_shader_module(const std::string& path, VkShaderModule& out) const -> bool + { + auto spv = load_spirv(path); + if (spv.empty()) { DONUT_ERROR("Vulkan: 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(device, &ci, nullptr, &out) == VK_SUCCESS; + } + + auto VulkanRenderer::Impl::create_geodesic_resources() -> bool + { + const VkFormat fmt = VK_FORMAT_R8G8B8A8_UNORM; + const VkMemoryPropertyFlags host_vis = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + const float SagA_rs = 1.269e10f; + + VkImageCreateInfo ici{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + ici.imageType = VK_IMAGE_TYPE_2D; ici.format = fmt; ici.extent = { GEO_W, GEO_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; + VK_CHECK(vkCreateImage(device, &ici, nullptr, &geo_image)); + VkMemoryRequirements im_req{}; vkGetImageMemoryRequirements(device, geo_image, &im_req); + VkMemoryAllocateInfo im_alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + im_alloc.allocationSize = im_req.size; + im_alloc.memoryTypeIndex = find_memory_type(im_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vkAllocateMemory(device, &im_alloc, nullptr, &geo_image_mem)); + VK_CHECK(vkBindImageMemory(device, geo_image, geo_image_mem, 0)); + VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + vci.image = geo_image; vci.viewType = VK_IMAGE_VIEW_TYPE_2D; vci.format = fmt; + vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + VK_CHECK(vkCreateImageView(device, &vci, nullptr, &geo_image_view)); + + VkAttachmentDescription color{}; + color.format = 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 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; + VK_CHECK(vkCreateRenderPass(device, &rpci, nullptr, &geo_render_pass)); + VkFramebufferCreateInfo fbci{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fbci.renderPass = geo_render_pass; fbci.attachmentCount = 1; fbci.pAttachments = &geo_image_view; + fbci.width = GEO_W; fbci.height = GEO_H; fbci.layers = 1; + VK_CHECK(vkCreateFramebuffer(device, &fbci, nullptr, &geo_framebuffer)); + + create_buffer(128, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, cam_buf, cam_mem); + create_buffer(32, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, disk_buf, disk_mem); + create_buffer(800, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, obj_buf, obj_mem); + create_buffer(16, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, host_vis, sim_buf, sim_mem); + + camera.set_camera_mode(CameraMode::Orbital); + camera.set_orbital_target(glm::vec3(0.0f)); + camera.set_orbital_radius(1e11); + camera.set_orbital_limits(4e10, 3e11); + camera.set_orbital_speed(0.01f); + camera.set_zoom_speed(1e10); + camera.set_azimuth(0.0f); + camera.set_elevation(1.25f); + + void* p = nullptr; + vkMapMemory(device, cam_mem, 0, 128, 0, &cam_mapped); // camera UBO is refilled every frame + + float disk_data[8] = { SagA_rs * 2.2f, SagA_rs * 5.2f, 2.0f, SagA_rs * 0.1f, 0.1f, 0, 0, 0 }; + vkMapMemory(device, disk_mem, 0, 32, 0, &p); memcpy(p, disk_data, sizeof(disk_data)); vkUnmapMemory(device, disk_mem); + + std::vector obj_data(800, 0); + int num_objects = 1; memcpy(obj_data.data(), &num_objects, 4); + float pos_radius[4] = { 0, 0, 0, SagA_rs }; memcpy(obj_data.data() + 16, pos_radius, 16); + float obj_color[4] = { 0, 0, 0, 1 }; memcpy(obj_data.data() + 272, obj_color, 16); + vkMapMemory(device, obj_mem, 0, 800, 0, &p); memcpy(p, obj_data.data(), 800); vkUnmapMemory(device, obj_mem); + + vkMapMemory(device, sim_mem, 0, 16, 0, &sim_mapped); + + if (!create_hdri_cubemap("assets/hdri/HDR_blue_nebulae-1.hdr")) return false; + + VkDescriptorSetLayoutBinding binds[5]{}; + for (int i = 0; i < 4; ++i) { binds[i].binding = i; binds[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; binds[i].descriptorCount = 1; binds[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } + binds[4].binding = 4; binds[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; binds[4].descriptorCount = 1; binds[4].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + dslci.bindingCount = 5; dslci.pBindings = binds; + VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &geo_set_layout)); + VkDescriptorPoolSize psizes[2] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 } }; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.maxSets = 1; dpci.poolSizeCount = 2; dpci.pPoolSizes = psizes; + VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &geo_pool)); + VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + dsai.descriptorPool = geo_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &geo_set_layout; + VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &geo_set)); + VkDescriptorBufferInfo bi[4] = { { cam_buf, 0, VK_WHOLE_SIZE }, { disk_buf, 0, VK_WHOLE_SIZE }, { obj_buf, 0, VK_WHOLE_SIZE }, { sim_buf, 0, VK_WHOLE_SIZE } }; + VkDescriptorImageInfo cube_info{ cube_sampler, cube_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet writes[5]{}; + for (int i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = geo_set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[i].pBufferInfo = &bi[i]; } + writes[4].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[4].dstSet = geo_set; writes[4].dstBinding = 4; writes[4].descriptorCount = 1; writes[4].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[4].pImageInfo = &cube_info; + vkUpdateDescriptorSets(device, 5, writes, 0, nullptr); + + float quad[] = { + -1.f, 1.f, 0.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, + -1.f, 1.f, 0.f, 1.f, 1.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, + }; + create_buffer(sizeof(quad), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, quad_vb, quad_vb_mem); + vkMapMemory(device, quad_vb_mem, 0, sizeof(quad), 0, &p); memcpy(p, quad, sizeof(quad)); vkUnmapMemory(device, quad_vb_mem); + + VkShaderModule vmod, fmod; + if (!create_shader_module("assets/shaders/generated/Geodesic.vertexMain.spv", vmod)) return false; + if (!create_shader_module("assets/shaders/generated/Geodesic.fragmentMain.spv", fmod)) return false; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + plci.setLayoutCount = 1; plci.pSetLayouts = &geo_set_layout; + VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &geo_pipeline_layout)); + 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, 16, VK_VERTEX_INPUT_RATE_VERTEX }; + VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; + vin.vertexAttributeDescriptionCount = 2; 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)GEO_W, (float)GEO_H, 0, 1 }; VkRect2D sc{ { 0, 0 }, { GEO_W, GEO_H } }; + 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; + 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 = geo_pipeline_layout; gpci.renderPass = geo_render_pass; gpci.subpass = 0; + VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &geo_pipeline); + vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); + if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: geodesic pipeline creation failed ({})", (int)pr); return false; } + + start_time = glfwGetTime(); + update_geodesic_uniforms(); + DONUT_INFO("Vulkan: geodesic resources ready ({}x{} offscreen)", (int)GEO_W, (int)GEO_H); + return true; + } + + auto VulkanRenderer::Impl::create_hdri_cubemap(const char* path) -> bool + { + 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; + + 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 = 1; 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_DST_BIT; + VK_CHECK(vkCreateImage(device, &cci, nullptr, &cube_image)); + VkMemoryRequirements creq{}; vkGetImageMemoryRequirements(device, cube_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); + VK_CHECK(vkAllocateMemory(device, &cai, nullptr, &cube_mem)); + VK_CHECK(vkBindImageMemory(device, cube_image, cube_mem, 0)); + VkImageViewCreateInfo cvci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + cvci.image = cube_image; cvci.viewType = VK_IMAGE_VIEW_TYPE_CUBE; cvci.format = cube_fmt; + cvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6 }; + VK_CHECK(vkCreateImageView(device, &cvci, nullptr, &cube_view)); + VkSamplerCreateInfo csm{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + csm.magFilter = VK_FILTER_LINEAR; csm.minFilter = VK_FILTER_LINEAR; + csm.addressModeU = csm.addressModeV = csm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VK_CHECK(vkCreateSampler(device, &csm, nullptr, &cube_sampler)); + + int w = 0, h = 0, ch = 0; + float* pixels = stbi_loadf(path, &w, &h, &ch, 4); + if (!pixels) + { + DONUT_WARN("Vulkan: HDRI '{}' could not be loaded; using a dark background", path); + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(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 = cube_image; tb.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 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, 1, 0, 6 }; + vkCmdClearColorImage(cmd, cube_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(graphics_queue, 1, &si, VK_NULL_HANDLE); vkQueueWaitIdle(graphics_queue); + vkFreeCommandBuffers(device, command_pool, 1, &cmd); + return true; + } + + // Apple GPUs can't linearly filter RGBA32F, so store the equirect as + // RGBA16F (convert the loaded floats to half on the way into staging). + 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; + if (!create_buffer(eq_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, host_vis, eq_staging, eq_staging_mem)) { stbi_image_free(pixels); return false; } + void* mp = nullptr; vkMapMemory(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]; memcpy(&dst[i], &hf, sizeof(uint16_t)); } + vkUnmapMemory(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; + VK_CHECK(vkCreateImage(device, &eci, nullptr, &eq_image)); + VkMemoryRequirements ereq{}; vkGetImageMemoryRequirements(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); + VK_CHECK(vkAllocateMemory(device, &eai, nullptr, &eq_mem)); + VK_CHECK(vkBindImageMemory(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 }; + VK_CHECK(vkCreateImageView(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; // longitude wraps + esm.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; // latitude clamps + esm.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VK_CHECK(vkCreateSampler(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 = cube_image; fvci.viewType = VK_IMAGE_VIEW_TYPE_2D; fvci.format = cube_fmt; + fvci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, i, 1 }; + VK_CHECK(vkCreateImageView(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; + VK_CHECK(vkCreateRenderPass(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; + VK_CHECK(vkCreateFramebuffer(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; + VK_CHECK(vkCreateDescriptorSetLayout(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; + VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &pool)); + + VkShaderModule vmod, fmod; + if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.vertexMain.spv", vmod)) return false; + if (!create_shader_module("assets/shaders/generated/EquirectToCubemap.fragmentMain.spv", fmod)) return false; + VkPipelineLayout playout; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; plci.setLayoutCount = 1; plci.pSetLayouts = &set_layout; + VK_CHECK(vkCreatePipelineLayout(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; + VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline); + vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); + if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: equirect pipeline failed ({})", (int)pr); return false; } + + 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(sizeof(cube_verts), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, host_vis, cube_vb, cube_vb_mem); + vkMapMemory(device, cube_vb_mem, 0, sizeof(cube_verts), 0, &mp); memcpy(mp, cube_verts, sizeof(cube_verts)); vkUnmapMemory(device, cube_vb_mem); + + glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); + proj[1][1] *= -1.0f; // Vulkan clip space is Y-down vs OpenGL + 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(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]) }; // SPIR-V expects row-major + vkMapMemory(device, ubo_mem[i], 0, 128, 0, &mp); memcpy(mp, mats, 128); vkUnmapMemory(device, ubo_mem[i]); + VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; dsai.descriptorPool = pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &set_layout; + VK_CHECK(vkAllocateDescriptorSets(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(device, 2, ws, 0, nullptr); + } + + VkCommandBufferAllocateInfo cbai{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + cbai.commandPool = command_pool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1; + VkCommandBuffer cmd; VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cmd)); + VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + VK_CHECK(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); + } + VK_CHECK(vkEndCommandBuffer(cmd)); + VkSubmitInfo si{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; + VK_CHECK(vkQueueSubmit(graphics_queue, 1, &si, VK_NULL_HANDLE)); + VK_CHECK(vkQueueWaitIdle(graphics_queue)); + + vkFreeCommandBuffers(device, command_pool, 1, &cmd); + for (uint32_t i = 0; i < 6; ++i) { vkDestroyBuffer(device, ubo[i], nullptr); vkFreeMemory(device, ubo_mem[i], nullptr); vkDestroyFramebuffer(device, face_fb[i], nullptr); vkDestroyImageView(device, face_views[i], nullptr); } + vkDestroyBuffer(device, cube_vb, nullptr); vkFreeMemory(device, cube_vb_mem, nullptr); + vkDestroyPipeline(device, pipeline, nullptr); vkDestroyPipelineLayout(device, playout, nullptr); + vkDestroyDescriptorPool(device, pool, nullptr); vkDestroyDescriptorSetLayout(device, set_layout, nullptr); + vkDestroyRenderPass(device, rp, nullptr); + vkDestroySampler(device, eq_sampler, nullptr); vkDestroyImageView(device, eq_view, nullptr); + vkDestroyImage(device, eq_image, nullptr); vkFreeMemory(device, eq_mem, nullptr); + vkDestroyBuffer(device, eq_staging, nullptr); vkFreeMemory(device, eq_staging_mem, nullptr); + DONUT_INFO("Vulkan: HDRI cubemap built from {} ({}x{} equirect -> {}^2 cube)", path, w, h, (int)FACE); + return true; + } + + auto VulkanRenderer::Impl::create_present_resources() -> bool + { + VkSamplerCreateInfo smci{ VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO }; + smci.magFilter = VK_FILTER_LINEAR; smci.minFilter = VK_FILTER_LINEAR; + smci.addressModeU = smci.addressModeV = smci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VK_CHECK(vkCreateSampler(device, &smci, nullptr, &present_sampler)); + + VkDescriptorSetLayoutBinding bind{}; bind.binding = 0; bind.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; bind.descriptorCount = 1; bind.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + dslci.bindingCount = 1; dslci.pBindings = &bind; + VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &present_set_layout)); + VkDescriptorPoolSize psize{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1 }; + VkDescriptorPoolCreateInfo dpci{ VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO }; + dpci.maxSets = 1; dpci.poolSizeCount = 1; dpci.pPoolSizes = &psize; + VK_CHECK(vkCreateDescriptorPool(device, &dpci, nullptr, &present_pool)); + VkDescriptorSetAllocateInfo dsai{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO }; + dsai.descriptorPool = present_pool; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &present_set_layout; + VK_CHECK(vkAllocateDescriptorSets(device, &dsai, &present_set)); + VkDescriptorImageInfo ii{ present_sampler, geo_image_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL }; + VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET }; + write.dstSet = present_set; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.pImageInfo = ⅈ + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + + VkShaderModule vmod, fmod; + if (!create_shader_module("assets/shaders/generated/TexturedQuad.vertexMain.spv", vmod)) return false; + if (!create_shader_module("assets/shaders/generated/TexturedQuad.fragmentMain.spv", fmod)) return false; + VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; + plci.setLayoutCount = 1; plci.pSetLayouts = &present_set_layout; + VK_CHECK(vkCreatePipelineLayout(device, &plci, nullptr, &present_pipeline_layout)); + 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, 16, VK_VERTEX_INPUT_RATE_VERTEX }; + VkVertexInputAttributeDescription via[2] = { { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }, { 1, 0, VK_FORMAT_R32G32_SFLOAT, 8 } }; + VkPipelineVertexInputStateCreateInfo vin{ VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO }; + vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &vib; + vin.vertexAttributeDescriptionCount = 2; vin.pVertexAttributeDescriptions = via; + VkPipelineInputAssemblyStateCreateInfo ia{ VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO }; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + 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_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; + 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.pDynamicState = &dsci; + gpci.pRasterizationState = &rs; gpci.pMultisampleState = &ms; gpci.pColorBlendState = &cb; + gpci.layout = present_pipeline_layout; gpci.renderPass = render_pass; gpci.subpass = 0; + VkResult pr = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &present_pipeline); + vkDestroyShaderModule(device, vmod, nullptr); vkDestroyShaderModule(device, fmod, nullptr); + if (pr != VK_SUCCESS) { DONUT_ERROR("Vulkan: present pipeline creation failed ({})", (int)pr); return false; } + DONUT_INFO("Vulkan: present pipeline ready"); + return true; + } + + auto VulkanRenderer::Impl::update_geodesic_uniforms() -> void + { + struct CamUBO { + glm::vec3 pos; float p0; glm::vec3 right; float p1; + glm::vec3 up; float p2; glm::vec3 fwd; float p3; + float tan_half_fov; float aspect; uint32_t moving; int p4; + } cam_data{}; + glm::vec3 pos = camera.get_orbital_position(); + glm::vec3 fwd = glm::normalize(camera.get_orbital_target() - pos); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + cam_data.pos = pos; cam_data.right = right; cam_data.up = glm::cross(right, fwd); cam_data.fwd = fwd; + cam_data.tan_half_fov = (float)tan(glm::radians(60.0f * 0.5f)); + cam_data.aspect = (float)GEO_W / (float)GEO_H; + cam_data.moving = camera.is_dragging() || camera.is_panning() ? 1u : 0u; + if (cam_mapped) memcpy(cam_mapped, &cam_data, sizeof(cam_data)); + + // Fewer integration steps while the camera moves keeps dragging responsive; + // more steps once it settles renders the disk in full. + struct SimUBO { int steps_moving; int steps_static; float early_exit; float time; } sim; + sim.steps_moving = 3500; sim.steps_static = 5000; sim.early_exit = 5e12f; + sim.time = (float)(glfwGetTime() - start_time); + if (sim_mapped) memcpy(sim_mapped, &sim, sizeof(sim)); + } + + static double g_ScrollAccum = 0.0; + static GLFWscrollfun g_PrevScroll = nullptr; + static void donut_vk_scroll_callback(GLFWwindow* w, double x, double y) + { + if (g_PrevScroll) g_PrevScroll(w, x, y); // keep ImGui's scroll handling intact + g_ScrollAccum += y; + } + + auto VulkanRenderer::Impl::process_input() -> void + { + bool over_ui = imgui_init && ImGui::GetIO().WantCaptureMouse; + + bool left_down = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + if (left_down && !left_was_down && !over_ui) + camera.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); + else if (!left_down && left_was_down) + camera.process_orbital_mouse_button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); + left_was_down = left_down; + + double mx = 0, my = 0; + glfwGetCursorPos(window, &mx, &my); + camera.process_orbital_mouse_move(mx, my); // tracks last position internally; orbits only while dragging + + double scroll = g_ScrollAccum; g_ScrollAccum = 0.0; + if (scroll != 0.0 && !over_ui) + camera.process_orbital_scroll(0.0, scroll); + } + + auto VulkanRenderer::Impl::destroy_geodesic_resources() -> void + { + if (present_pipeline) vkDestroyPipeline(device, present_pipeline, nullptr); + if (present_pipeline_layout) vkDestroyPipelineLayout(device, present_pipeline_layout, nullptr); + if (present_pool) vkDestroyDescriptorPool(device, present_pool, nullptr); + if (present_set_layout) vkDestroyDescriptorSetLayout(device, present_set_layout, nullptr); + if (present_sampler) vkDestroySampler(device, present_sampler, nullptr); + + if (geo_pipeline) vkDestroyPipeline(device, geo_pipeline, nullptr); + if (geo_pipeline_layout) vkDestroyPipelineLayout(device, geo_pipeline_layout, nullptr); + if (geo_pool) vkDestroyDescriptorPool(device, geo_pool, nullptr); + if (geo_set_layout) vkDestroyDescriptorSetLayout(device, geo_set_layout, nullptr); + if (quad_vb) vkDestroyBuffer(device, quad_vb, nullptr); + if (quad_vb_mem) vkFreeMemory(device, quad_vb_mem, nullptr); + if (cube_sampler) vkDestroySampler(device, cube_sampler, nullptr); + if (cube_view) vkDestroyImageView(device, cube_view, nullptr); + if (cube_image) vkDestroyImage(device, cube_image, nullptr); + if (cube_mem) vkFreeMemory(device, cube_mem, nullptr); + if (cam_mapped) { vkUnmapMemory(device, cam_mem); cam_mapped = nullptr; } + if (sim_mapped) { vkUnmapMemory(device, sim_mem); sim_mapped = nullptr; } + VkBuffer ubos[4] = { cam_buf, disk_buf, obj_buf, sim_buf }; + VkDeviceMemory umem[4] = { cam_mem, disk_mem, obj_mem, sim_mem }; + for (int i = 0; i < 4; ++i) { if (ubos[i]) vkDestroyBuffer(device, ubos[i], nullptr); if (umem[i]) vkFreeMemory(device, umem[i], nullptr); } + if (geo_framebuffer) vkDestroyFramebuffer(device, geo_framebuffer, nullptr); + if (geo_render_pass) vkDestroyRenderPass(device, geo_render_pass, nullptr); + if (geo_image_view) vkDestroyImageView(device, geo_image_view, nullptr); + if (geo_image) vkDestroyImage(device, geo_image, nullptr); + if (geo_image_mem) vkFreeMemory(device, geo_image_mem, nullptr); + } + + auto VulkanRenderer::Impl::record_command_buffer(VkCommandBuffer cmd, uint32_t image_index, const glm::vec4& clear, ImDrawData* draw_data) -> bool + { + VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + VK_CHECK(vkBeginCommandBuffer(cmd, &begin)); + + // Geodesic offscreen pass + VkClearValue geo_clear{}; geo_clear.color = { { 0, 0, 0, 1 } }; + VkRenderPassBeginInfo grp{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + grp.renderPass = geo_render_pass; grp.framebuffer = geo_framebuffer; + grp.renderArea = { { 0, 0 }, { GEO_W, GEO_H } }; + grp.clearValueCount = 1; grp.pClearValues = &geo_clear; + vkCmdBeginRenderPass(cmd, &grp, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, geo_pipeline_layout, 0, 1, &geo_set, 0, nullptr); + VkDeviceSize off = 0; vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off); + vkCmdDraw(cmd, 6, 1, 0, 0); + vkCmdEndRenderPass(cmd); + + // Swapchain pass: upscale the geodesic image, then the ImGui UI on top + VkClearValue cv{}; cv.color = { { clear.r, clear.g, clear.b, clear.a } }; + VkRenderPassBeginInfo rpbi{ VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO }; + rpbi.renderPass = render_pass; + rpbi.framebuffer = framebuffers[image_index]; + rpbi.renderArea = { { 0, 0 }, swapchain_extent }; + rpbi.clearValueCount = 1; rpbi.pClearValues = &cv; + vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline); + // Negative-height viewport flips the geodesic image vertically so the scene + // reads the same as the OpenGL path (Vulkan's clip space is Y-down). Only + // this draw is affected; ImGui sets its own viewport. + VkViewport vp{ 0, (float)swapchain_extent.height, (float)swapchain_extent.width, -(float)swapchain_extent.height, 0, 1 }; + VkRect2D scissor{ { 0, 0 }, swapchain_extent }; + vkCmdSetViewport(cmd, 0, 1, &vp); + vkCmdSetScissor(cmd, 0, 1, &scissor); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, present_pipeline_layout, 0, 1, &present_set, 0, nullptr); + vkCmdBindVertexBuffers(cmd, 0, 1, &quad_vb, &off); + vkCmdDraw(cmd, 6, 1, 0, 0); + if (draw_data) + ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); + vkCmdEndRenderPass(cmd); + + VK_CHECK(vkEndCommandBuffer(cmd)); + return true; + } + + auto VulkanRenderer::Impl::cleanup_swapchain() -> void + { + for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr); + framebuffers.clear(); + for (auto iv : image_views) vkDestroyImageView(device, iv, nullptr); + image_views.clear(); + if (render_pass) { vkDestroyRenderPass(device, render_pass, nullptr); render_pass = VK_NULL_HANDLE; } + if (swapchain) { vkDestroySwapchainKHR(device, swapchain, nullptr); swapchain = VK_NULL_HANDLE; } + } + + auto VulkanRenderer::Impl::recreate_swapchain() -> bool + { + // Wait until the window has a non-zero size (e.g. after un-minimizing). + int w = 0, h = 0; + glfwGetFramebufferSize(window, &w, &h); + while (w == 0 || h == 0) + { + glfwGetFramebufferSize(window, &w, &h); + glfwWaitEvents(); + } + width = w; height = h; + vkDeviceWaitIdle(device); + + cleanup_swapchain(); + // render_finished are tied to image count; recreate below via sync if it changed. + if (!create_swapchain()) return false; + if (!create_image_views()) return false; + if (!create_render_pass()) return false; + if (!create_framebuffers())return false; + images_in_flight.assign(images.size(), VK_NULL_HANDLE); + return true; + } + + VulkanRenderer::VulkanRenderer() { m_impl = new Impl(); } + VulkanRenderer::~VulkanRenderer() { shutdown(); delete m_impl; m_impl = nullptr; } + + auto VulkanRenderer::init(void* glfwWindow, int width, int height) -> bool + { + Impl& v = *m_impl; + v.window = (GLFWwindow*)glfwWindow; + v.width = width; v.height = height; + + if (!v.create_instance()) return false; + if (!v.pick_physical_and_device()) return false; + if (!v.create_swapchain()) return false; + if (!v.create_image_views()) return false; + if (!v.create_render_pass()) return false; + if (!v.create_framebuffers()) return false; + if (!v.create_command_buffers()) return false; + if (!v.create_sync_objects()) return false; + if (!v.create_geodesic_resources()) return false; + if (!v.create_present_resources()) return false; + + DONUT_INFO("Vulkan renderer ready: {} swapchain images, {}x{}", + (int)v.images.size(), v.swapchain_extent.width, v.swapchain_extent.height); + return true; + } + + auto VulkanRenderer::init_im_gui() -> bool + { + Impl& v = *m_impl; + if (v.device == VK_NULL_HANDLE) return false; + + 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; + VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &v.imgui_pool)); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui::StyleColorsDark(); + + ImGui_ImplGlfw_InitForVulkan(v.window, true); + g_PrevScroll = glfwSetScrollCallback(v.window, donut_vk_scroll_callback); // chain ImGui + camera zoom + ImGui_ImplVulkan_InitInfo info{}; + info.ApiVersion = VK_API_VERSION_1_2; + info.Instance = v.instance; + info.PhysicalDevice = v.physical; + info.Device = v.device; + info.QueueFamily = v.graphics_family; + info.Queue = v.graphics_queue; + info.DescriptorPool = v.imgui_pool; + info.RenderPass = v.render_pass; + info.MinImageCount = 2; + info.ImageCount = (uint32_t)v.images.size(); + info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + if (!ImGui_ImplVulkan_Init(&info)) + { + DONUT_ERROR("Vulkan: ImGui_ImplVulkan_Init failed"); + return false; + } + + v.imgui_init = true; + DONUT_INFO("Vulkan: ImGui backend initialized"); + return true; + } + + auto VulkanRenderer::on_resize(int width, int height) -> void + { + m_impl->framebuffer_resized = true; + m_impl->width = width; m_impl->height = height; + } + + auto VulkanRenderer::draw_frame(const glm::vec4& clear_color, const std::function& build_ui) -> void + { + Impl& v = *m_impl; + if (v.device == VK_NULL_HANDLE) return; + + ImDrawData* draw_data = nullptr; + if (v.imgui_init) + { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + if (build_ui) build_ui(); + ImGui::Render(); + draw_data = ImGui::GetDrawData(); + } + + vkWaitForFences(v.device, 1, &v.in_flight[v.current_frame], VK_TRUE, UINT64_MAX); + + uint32_t image_index = 0; + VkResult r = vkAcquireNextImageKHR(v.device, v.swapchain, UINT64_MAX, + v.image_available[v.current_frame], VK_NULL_HANDLE, &image_index); + if (r == VK_ERROR_OUT_OF_DATE_KHR) { v.recreate_swapchain(); return; } + if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) { DONUT_ERROR("Vulkan: acquire failed ({})", (int)r); return; } + + if (v.images_in_flight[image_index] != VK_NULL_HANDLE) + vkWaitForFences(v.device, 1, &v.images_in_flight[image_index], VK_TRUE, UINT64_MAX); + v.images_in_flight[image_index] = v.in_flight[v.current_frame]; + + // The geodesic offscreen image is shared across frames in flight; wait for + // the previous frame to finish reading it before overwriting it this frame. + if (v.geo_in_use != VK_NULL_HANDLE) + vkWaitForFences(v.device, 1, &v.geo_in_use, VK_TRUE, UINT64_MAX); + v.process_input(); + v.update_geodesic_uniforms(); + + vkResetCommandBuffer(v.command_buffers[v.current_frame], 0); + if (!v.record_command_buffer(v.command_buffers[v.current_frame], image_index, clear_color, draw_data)) return; + + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; + submit.waitSemaphoreCount = 1; + submit.pWaitSemaphores = &v.image_available[v.current_frame]; + submit.pWaitDstStageMask = &wait_stage; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &v.command_buffers[v.current_frame]; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &v.render_finished[image_index]; + + vkResetFences(v.device, 1, &v.in_flight[v.current_frame]); + if (vkQueueSubmit(v.graphics_queue, 1, &submit, v.in_flight[v.current_frame]) != VK_SUCCESS) + { DONUT_ERROR("Vulkan: queue submit failed"); return; } + v.geo_in_use = v.in_flight[v.current_frame]; + + VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + present.waitSemaphoreCount = 1; + present.pWaitSemaphores = &v.render_finished[image_index]; + present.swapchainCount = 1; + present.pSwapchains = &v.swapchain; + present.pImageIndices = &image_index; + r = vkQueuePresentKHR(v.present_queue, &present); + if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR || v.framebuffer_resized) + { + v.framebuffer_resized = false; + v.recreate_swapchain(); + } + + v.current_frame = (v.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; + } + + auto VulkanRenderer::shutdown() -> void + { + Impl& v = *m_impl; + if (v.device == VK_NULL_HANDLE) { if (v.instance && v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } return; } + + vkDeviceWaitIdle(v.device); + v.destroy_geodesic_resources(); + if (v.imgui_init) + { + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + v.imgui_init = false; + } + if (v.imgui_pool) { vkDestroyDescriptorPool(v.device, v.imgui_pool, nullptr); v.imgui_pool = VK_NULL_HANDLE; } + for (auto s : v.render_finished) vkDestroySemaphore(v.device, s, nullptr); + for (auto s : v.image_available) vkDestroySemaphore(v.device, s, nullptr); + for (auto f : v.in_flight) vkDestroyFence(v.device, f, nullptr); + v.render_finished.clear(); v.image_available.clear(); v.in_flight.clear(); + if (v.command_pool) { vkDestroyCommandPool(v.device, v.command_pool, nullptr); v.command_pool = VK_NULL_HANDLE; } + v.cleanup_swapchain(); + vkDestroyDevice(v.device, nullptr); v.device = VK_NULL_HANDLE; + if (v.surface) { vkDestroySurfaceKHR(v.instance, v.surface, nullptr); v.surface = VK_NULL_HANDLE; } + if (v.instance) { vkDestroyInstance(v.instance, nullptr); v.instance = VK_NULL_HANDLE; } + } +} diff --git a/src/platform/vulkan/vulkan_renderer.h b/src/platform/vulkan/vulkan_renderer.h new file mode 100644 index 0000000..97a4e38 --- /dev/null +++ b/src/platform/vulkan/vulkan_renderer.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +// Live-window Vulkan backend: owns the instance, surface, device, swapchain, +// render pass, framebuffers and per-frame synchronization, and drives the +// acquire -> record -> submit -> present loop. Pure-C++ header (no vulkan.h / +// glfw leak); the GLFW window is passed as an opaque handle. +namespace Donut +{ + // Must be called BEFORE glfwInit() when the Vulkan API is selected: points + // GLFW at the loader the app links against (GLFW's own dlopen fails on + // macOS/Homebrew) and configures the MoltenVK ICD / layer paths. + auto vulkan_prepare_glfw() -> void; + + class VulkanRenderer + { + public: + VulkanRenderer(); + ~VulkanRenderer(); + + // glfwWindow must be a GLFW window created with GLFW_NO_API. + auto init(void* glfwWindow, int width, int height) -> bool; + auto shutdown() -> void; + + // Creates the ImGui context + Vulkan/GLFW backends. Call after init(). + auto init_im_gui() -> bool; + + // Renders + presents one frame: clears to clearColor, then (if init_im_gui + // ran) opens an ImGui frame, invokes buildUI to populate it, and draws it. + auto draw_frame(const glm::vec4& clearColor, const std::function& buildUI = {}) -> void; + + auto on_resize(int width, int height) -> void; + + private: + struct Impl; + Impl* m_impl = nullptr; + }; +} diff --git a/src/platform/vulkan/vulkan_renderer_api.cpp b/src/platform/vulkan/vulkan_renderer_api.cpp new file mode 100644 index 0000000..56499f4 --- /dev/null +++ b/src/platform/vulkan/vulkan_renderer_api.cpp @@ -0,0 +1,86 @@ +#include "vulkan_renderer_api.h" + +namespace Donut +{ + auto VulkanRendererAPI::init() -> void + { + // TODO(Hachem): Implement Vulkan renderer API initialization + } + + auto VulkanRendererAPI::set_viewport(uint32_t x, uint32_t y, uint32_t width, uint32_t height) -> void + { + // TODO(Hachem): Implement Vulkan viewport setting + } + + auto VulkanRendererAPI::set_clear_color(const glm::vec4& color) -> void + { + // TODO(Hachem): Implement Vulkan clear color setting + } + + auto VulkanRendererAPI::clear() -> void + { + // TODO(Hachem): Implement Vulkan clear + } + + auto VulkanRendererAPI::enable_depth_test() -> void + { + // TODO(Hachem): Implement Vulkan depth test enabling + } + + auto VulkanRendererAPI::disable_depth_test() -> void + { + // TODO(Hachem): Implement Vulkan depth test disabling + } + + auto VulkanRendererAPI::set_face_culling(bool enabled) -> void + { + // TODO(Hachem): Implement Vulkan face culling setting + } + + auto VulkanRendererAPI::enable_blending() -> void + { + // TODO(Hachem): Implement Vulkan blending enabling + } + + auto VulkanRendererAPI::disable_blending() -> void + { + // TODO(Hachem): Implement Vulkan blending disabling + } + + auto VulkanRendererAPI::draw_indexed(const Ref& vertex_array, uint32_t index_count) -> void + { + // TODO(Hachem): Implement Vulkan indexed drawing + } + + auto VulkanRendererAPI::draw_arrays(uint32_t vertex_count, uint32_t first) -> void + { + // TODO(Hachem): Implement Vulkan array drawing + } + + auto VulkanRendererAPI::draw_lines(const Ref& vertex_array, uint32_t index_count) -> void + { + // TODO(Hachem): Implement Vulkan line drawing + } + + auto VulkanRendererAPI::bind_texture(uint32_t texture_id, uint32_t slot) -> void + { + // TODO(Hachem): Implement Vulkan texture binding + } + + auto VulkanRendererAPI::bind_image_texture(uint32_t texture_id, uint32_t slot, bool read_only) -> void + { + // TODO(Hachem): Implement Vulkan image texture binding + } + + auto VulkanRendererAPI::read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) -> void + { + // TODO(Hachem): Implement Vulkan pixel reading + // For now, this is a placeholder implementation + // In a real Vulkan implementation, this would involve: + // 1. Creating a staging buffer + // 2. Copying the framebuffer to the staging buffer + // 3. Mapping the staging buffer and copying to the pixels array + // 4. Unmapping and destroying the staging buffer + } +}; diff --git a/src/platform/vulkan/vulkan_renderer_api.h b/src/platform/vulkan/vulkan_renderer_api.h new file mode 100644 index 0000000..15bf76d --- /dev/null +++ b/src/platform/vulkan/vulkan_renderer_api.h @@ -0,0 +1,38 @@ +#pragma once + +#include "core/memory.h" +#include "rendering/renderer.h" + +namespace Donut +{ + class VulkanRendererAPI + : public RendererAPI + { + public: + virtual auto init() -> void override; + virtual void set_viewport(uint32_t x, uint32_t y, + uint32_t width, uint32_t height) override; + virtual auto set_clear_color(const glm::vec4& color) -> void override; + virtual auto clear() -> void override; + virtual auto enable_depth_test() -> void override; + virtual auto disable_depth_test() -> void override; + virtual auto set_face_culling(bool enabled) -> void override; + virtual auto enable_blending() -> void override; + virtual auto disable_blending() -> void override; + + virtual void draw_indexed(const Ref& vertex_array, + uint32_t index_count = 0) override; + + virtual void draw_arrays(uint32_t vertex_count, + uint32_t first = 0) override; + virtual void draw_lines(const Ref& vertex_array, + uint32_t index_count = 0) override; + virtual void bind_texture(uint32_t texture_id, + uint32_t slot = 0) override; + virtual void bind_image_texture(uint32_t texture_id, + uint32_t slot = 0, + bool read_only = false) override; + virtual void read_pixels(uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t format, uint32_t type, void* pixels) override; + }; +}; diff --git a/src/platform/vulkan/vulkan_shader.cpp b/src/platform/vulkan/vulkan_shader.cpp new file mode 100644 index 0000000..7549761 --- /dev/null +++ b/src/platform/vulkan/vulkan_shader.cpp @@ -0,0 +1,146 @@ +#include "vulkan_shader.h" + +#include +#include + +namespace Donut +{ + VulkanShader::VulkanShader(const std::string& filepath) + { + // TODO(Hachem): Implement Vulkan shader creation from filepath + } + + VulkanShader::VulkanShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src) + : m_name(name) + { + // TODO(Hachem): Implement Vulkan shader creation from source + } + + VulkanShader::VulkanShader(const std::string& name, const std::string& compute_src) + : m_name(name) + { + // TODO(Hachem): Implement Vulkan compute shader creation + } + + VulkanShader::~VulkanShader() + { + // TODO(Hachem): Implement Vulkan shader cleanup + } + + auto VulkanShader::bind() const -> void + { + // TODO(Hachem): Implement Vulkan shader binding + } + + auto VulkanShader::unbind() const -> void + { + // TODO(Hachem): Implement Vulkan shader unbinding + } + + auto VulkanShader::set_int(const std::string& name, int value) -> void + { + // TODO(Hachem): Implement Vulkan shader int uniform setting + } + + auto VulkanShader::set_int_array(const std::string& name, int* values, uint32_t count) -> void + { + // TODO(Hachem): Implement Vulkan shader int array uniform setting + } + + auto VulkanShader::set_float(const std::string& name, float value) -> void + { + // TODO(Hachem): Implement Vulkan shader float uniform setting + } + + auto VulkanShader::set_float2(const std::string& name, const glm::vec2& value) -> void + { + // TODO(Hachem): Implement Vulkan shader float2 uniform setting + } + + auto VulkanShader::set_float3(const std::string& name, const glm::vec3& value) -> void + { + // TODO(Hachem): Implement Vulkan shader float3 uniform setting + } + + auto VulkanShader::set_float4(const std::string& name, const glm::vec4& value) -> void + { + // TODO(Hachem): Implement Vulkan shader float4 uniform setting + } + + auto VulkanShader::set_mat4(const std::string& name, const glm::mat4& value) -> void + { + // TODO(Hachem): Implement Vulkan shader mat4 uniform setting + } + + auto VulkanShader::dispatch(uint32_t x, uint32_t y, uint32_t z) -> void + { + // TODO(Hachem): Implement Vulkan compute shader dispatch + } + + auto VulkanShader::dispatch_indirect(uint32_t offset) -> void + { + // TODO(Hachem): Implement Vulkan indirect compute shader dispatch + } + + auto VulkanShader::memory_barrier(uint32_t barriers) -> void + { + // TODO(Hachem): Implement Vulkan memory barrier + } + + auto VulkanShader::upload_uniform_int(const std::string& name, int value) -> void + { + // TODO(Hachem): Implement Vulkan uniform int upload + } + + auto VulkanShader::upload_uniform_int_array(const std::string& name, int* values, uint32_t count) -> void + { + // TODO(Hachem): Implement Vulkan uniform int array upload + } + + auto VulkanShader::upload_uniform_float(const std::string& name, float value) -> void + { + // TODO(Hachem): Implement Vulkan uniform float upload + } + + auto VulkanShader::upload_uniform_float2(const std::string& name, const glm::vec2& value) -> void + { + // TODO(Hachem): Implement Vulkan uniform float2 upload + } + + auto VulkanShader::upload_uniform_float3(const std::string& name, const glm::vec3& value) -> void + { + // TODO(Hachem): Implement Vulkan uniform float3 upload + } + + auto VulkanShader::upload_uniform_float4(const std::string& name, const glm::vec4& value) -> void + { + // TODO(Hachem): Implement Vulkan uniform float4 upload + } + + auto VulkanShader::upload_uniform_mat3(const std::string& name, const glm::mat3& matrix) -> void + { + // TODO(Hachem): Implement Vulkan uniform mat3 upload + } + + auto VulkanShader::upload_uniform_mat4(const std::string& name, const glm::mat4& matrix) -> void + { + // TODO(Hachem): Implement Vulkan uniform mat4 upload + } + + auto VulkanShader::read_file(const std::string& filepath) -> std::string + { + // TODO(Hachem): Implement file reading for Vulkan shader + return ""; + } + + auto VulkanShader::pre_process(const std::string& source) -> std::unordered_map + { + // TODO(Hachem): Implement shader preprocessing for Vulkan + return {}; + } + + auto VulkanShader::compile(const std::unordered_map& shader_sources) -> void + { + // TODO(Hachem): Implement Vulkan shader compilation + } +}; diff --git a/src/platform/vulkan/vulkan_shader.h b/src/platform/vulkan/vulkan_shader.h new file mode 100644 index 0000000..9453130 --- /dev/null +++ b/src/platform/vulkan/vulkan_shader.h @@ -0,0 +1,52 @@ +#pragma once + +#include "rendering/shader.h" + +#include +#include + +namespace Donut +{ + class VulkanShader + : public Shader + { + public: + VulkanShader(const std::string& filepath); + VulkanShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src); + VulkanShader(const std::string& name, const std::string& compute_src); + virtual ~VulkanShader(); + + virtual auto bind() const -> void override; + virtual auto unbind() const -> void override; + + virtual auto set_int(const std::string& name, int value) -> void override; + virtual auto set_int_array(const std::string& name, int* values, uint32_t count) -> void override; + virtual auto set_float(const std::string& name, float value) -> void override; + virtual auto set_float2(const std::string& name, const glm::vec2& value) -> void override; + virtual auto set_float3(const std::string& name, const glm::vec3& value) -> void override; + virtual auto set_float4(const std::string& name, const glm::vec4& value) -> void override; + virtual auto set_mat4(const std::string& name, const glm::mat4& value) -> void override; + + virtual auto dispatch(uint32_t x, uint32_t y = 1, uint32_t z = 1) -> void override; + virtual auto dispatch_indirect(uint32_t offset = 0) -> void override; + virtual auto memory_barrier(uint32_t barriers) -> void override; + + virtual auto get_name() const -> const std::string& override{ return m_name; } + virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; } + + auto upload_uniform_int(const std::string& name, int value) -> void; + auto upload_uniform_int_array(const std::string& name, int* values, uint32_t count) -> void; + auto upload_uniform_float(const std::string& name, float value) -> void; + auto upload_uniform_float2(const std::string& name, const glm::vec2& value) -> void; + auto upload_uniform_float3(const std::string& name, const glm::vec3& value) -> void; + auto upload_uniform_float4(const std::string& name, const glm::vec4& value) -> void; + auto upload_uniform_mat3(const std::string& name, const glm::mat3& matrix) -> void; + auto upload_uniform_mat4(const std::string& name, const glm::mat4& matrix) -> void; + private: + auto read_file(const std::string& filepath) -> std::string; + auto pre_process(const std::string& source) -> std::unordered_map; + auto compile(const std::unordered_map& shader_sources) -> void; + uint32_t m_renderer_id; + std::string m_name; + }; +}; diff --git a/src/platform/vulkan/vulkan_texture.cpp b/src/platform/vulkan/vulkan_texture.cpp new file mode 100644 index 0000000..a970a53 --- /dev/null +++ b/src/platform/vulkan/vulkan_texture.cpp @@ -0,0 +1,73 @@ +#include "vulkan_texture.h" + +namespace Donut +{ + VulkanTexture2D::VulkanTexture2D(uint32_t width, uint32_t height) + : m_width(width), m_height(height) + { + m_internal_format = 0; + m_data_format = 0; + m_renderer_id = 0; + } + + VulkanTexture2D::VulkanTexture2D(const std::string& path) + : m_path(path) + { + m_width = 1; + m_height = 1; + m_internal_format = 0; + m_data_format = 0; + m_renderer_id = 0; + } + + VulkanTexture2D::~VulkanTexture2D() + { + } + + auto VulkanTexture2D::set_data(void* data, uint32_t size) -> void + { + } + + auto VulkanTexture2D::bind(uint32_t slot) const -> void + { + } + + auto VulkanTexture2D::bind_as_image(uint32_t slot, bool read_only) const -> void + { + } + + // Vulkan Cubemap Implementation (Placeholder) + VulkanCubemapTexture::VulkanCubemapTexture(uint32_t width, uint32_t height) + : m_width(width), m_height(height) + { + m_internal_format = 0; + m_data_format = 0; + m_renderer_id = 0; + } + + VulkanCubemapTexture::VulkanCubemapTexture(const std::string& path) + : m_path(path) + { + m_width = 1024; + m_height = 1024; + m_internal_format = 0; + m_data_format = 0; + m_renderer_id = 0; + } + + VulkanCubemapTexture::~VulkanCubemapTexture() + { + } + + auto VulkanCubemapTexture::set_data(void* data, uint32_t size) -> void + { + } + + auto VulkanCubemapTexture::bind(uint32_t slot) const -> void + { + } + + auto VulkanCubemapTexture::bind_as_image(uint32_t slot, bool read_only) const -> void + { + } +}; diff --git a/src/platform/vulkan/vulkan_texture.h b/src/platform/vulkan/vulkan_texture.h new file mode 100644 index 0000000..ea8471b --- /dev/null +++ b/src/platform/vulkan/vulkan_texture.h @@ -0,0 +1,60 @@ +#pragma once + +#include "rendering/texture.h" + +namespace Donut +{ + class VulkanTexture2D + : public Texture2D + { + public: + VulkanTexture2D(uint32_t width, uint32_t height); + VulkanTexture2D(const std::string& path); + virtual ~VulkanTexture2D(); + + virtual auto get_width() const -> uint32_t override{ return m_width; } + virtual auto get_height() const -> uint32_t override{ return m_height; } + virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; } + + virtual auto set_data(void* data, uint32_t size) -> void override; + virtual auto bind(uint32_t slot = 0) const -> void override; + virtual auto bind_as_image(uint32_t slot = 0, bool read_only = false) const -> void override; + + virtual bool operator==(const Texture& other) const override + { + return m_renderer_id == ((VulkanTexture2D&)other).m_renderer_id; + } + private: + std::string m_path; + uint32_t m_width, m_height; + uint32_t m_renderer_id; + uint32_t m_internal_format, m_data_format; + }; + + class VulkanCubemapTexture + : public CubemapTexture + { + public: + VulkanCubemapTexture(uint32_t width, uint32_t height); + VulkanCubemapTexture(const std::string& path); + virtual ~VulkanCubemapTexture(); + + virtual auto get_width() const -> uint32_t override{ return m_width; } + virtual auto get_height() const -> uint32_t override{ return m_height; } + virtual auto get_renderer_id() const -> uint32_t override{ return m_renderer_id; } + + virtual auto set_data(void* data, uint32_t size) -> void override; + virtual auto bind(uint32_t slot = 0) const -> void override; + virtual auto bind_as_image(uint32_t slot = 0, bool read_only = false) const -> void override; + + virtual bool operator==(const Texture& other) const override + { + return m_renderer_id == ((VulkanCubemapTexture&)other).m_renderer_id; + } + private: + std::string m_path; + uint32_t m_width, m_height; + uint32_t m_renderer_id; + uint32_t m_internal_format, m_data_format; + }; +}; diff --git a/src/platform/vulkan/vulkan_uniform_buffer.cpp b/src/platform/vulkan/vulkan_uniform_buffer.cpp new file mode 100644 index 0000000..1a64099 --- /dev/null +++ b/src/platform/vulkan/vulkan_uniform_buffer.cpp @@ -0,0 +1,25 @@ +#include "vulkan_uniform_buffer.h" + +namespace Donut +{ + VulkanUniformBuffer::VulkanUniformBuffer(uint32_t size, uint32_t binding) + : m_size(size), m_binding(binding) + { + // TODO: Implement Vulkan uniform buffer + } + + VulkanUniformBuffer::~VulkanUniformBuffer() + { + // TODO: Implement Vulkan uniform buffer cleanup + } + + auto VulkanUniformBuffer::set_data(const void* data, uint32_t size, uint32_t offset) -> void + { + // TODO: Implement Vulkan uniform buffer data setting + } + + auto VulkanUniformBuffer::bind(uint32_t binding) -> void + { + // TODO: Implement Vulkan uniform buffer binding + } +}; diff --git a/src/platform/vulkan/vulkan_uniform_buffer.h b/src/platform/vulkan/vulkan_uniform_buffer.h new file mode 100644 index 0000000..7d9ad0a --- /dev/null +++ b/src/platform/vulkan/vulkan_uniform_buffer.h @@ -0,0 +1,19 @@ +#pragma once + +#include "rendering/uniform_buffer.h" + +namespace Donut +{ + class VulkanUniformBuffer : public UniformBuffer + { + public: + VulkanUniformBuffer(uint32_t size, uint32_t binding); + virtual ~VulkanUniformBuffer(); + + virtual auto set_data(const void* data, uint32_t size, uint32_t offset = 0) -> void override; + virtual auto bind(uint32_t binding) -> void override; + private: + uint32_t m_size; + uint32_t m_binding; + }; +}; diff --git a/src/platform/vulkan/vulkan_vertex_array.cpp b/src/platform/vulkan/vulkan_vertex_array.cpp new file mode 100644 index 0000000..73219b1 --- /dev/null +++ b/src/platform/vulkan/vulkan_vertex_array.cpp @@ -0,0 +1,36 @@ +#include "vulkan_vertex_array.h" + +namespace Donut +{ + VulkanVertexArray::VulkanVertexArray() + { + // TODO(Hachem): Implement Vulkan vertex array creation + } + + VulkanVertexArray::~VulkanVertexArray() + { + // TODO(Hachem): Implement Vulkan vertex array cleanup + } + + auto VulkanVertexArray::bind() const -> void + { + // TODO(Hachem): Implement Vulkan vertex array binding + } + + auto VulkanVertexArray::unbind() const -> void + { + // TODO(Hachem): Implement Vulkan vertex array unbinding + } + + auto VulkanVertexArray::add_vertex_buffer(const Ref& vertex_buffer) -> void + { + // TODO(Hachem): Implement Vulkan vertex buffer addition + m_vertex_buffers.push_back(vertex_buffer); + } + + auto VulkanVertexArray::set_index_buffer(const Ref& index_buffer) -> void + { + // TODO(Hachem): Implement Vulkan index buffer setting + m_index_buffer = index_buffer; + } +}; diff --git a/src/platform/vulkan/vulkan_vertex_array.h b/src/platform/vulkan/vulkan_vertex_array.h new file mode 100644 index 0000000..0d282a2 --- /dev/null +++ b/src/platform/vulkan/vulkan_vertex_array.h @@ -0,0 +1,28 @@ +#pragma once + +#include "rendering/vertex_array.h" +#include "core/memory.h" + +namespace Donut +{ + class VulkanVertexArray + : public VertexArray + { + public: + VulkanVertexArray(); + virtual ~VulkanVertexArray(); + + virtual auto bind() const -> void override; + virtual auto unbind() const -> void override; + + virtual auto add_vertex_buffer(const Ref& vertex_buffer) -> void override; + virtual auto set_index_buffer(const Ref& index_buffer) -> void override; + + virtual auto get_vertex_buffers() const -> const std::vector>& { return m_vertex_buffers; } + virtual auto get_index_buffer() const -> const Ref& { return m_index_buffer; } + private: + uint32_t m_renderer_id; + std::vector> m_vertex_buffers; + Ref m_index_buffer; + }; +}; diff --git a/src/platform/vulkan/vulkan_vertex_buffer.cpp b/src/platform/vulkan/vulkan_vertex_buffer.cpp new file mode 100644 index 0000000..6a222c1 --- /dev/null +++ b/src/platform/vulkan/vulkan_vertex_buffer.cpp @@ -0,0 +1,34 @@ +#include "vulkan_vertex_buffer.h" + +namespace Donut +{ + VulkanVertexBuffer::VulkanVertexBuffer(uint32_t size) + { + // TODO(Hachem): Implement Vulkan vertex buffer creation with size + } + + VulkanVertexBuffer::VulkanVertexBuffer(float* vertices, uint32_t size) + { + // TODO(Hachem): Implement Vulkan vertex buffer creation with data + } + + VulkanVertexBuffer::~VulkanVertexBuffer() + { + // TODO(Hachem): Implement Vulkan vertex buffer cleanup + } + + auto VulkanVertexBuffer::bind() const -> void + { + // TODO(Hachem): Implement Vulkan vertex buffer binding + } + + auto VulkanVertexBuffer::unbind() const -> void + { + // TODO(Hachem): Implement Vulkan vertex buffer unbinding + } + + auto VulkanVertexBuffer::set_data(const void* data, uint32_t size) -> void + { + // TODO(Hachem): Implement Vulkan vertex buffer data setting + } +}; \ No newline at end of file diff --git a/src/platform/vulkan/vulkan_vertex_buffer.h b/src/platform/vulkan/vulkan_vertex_buffer.h new file mode 100644 index 0000000..3218021 --- /dev/null +++ b/src/platform/vulkan/vulkan_vertex_buffer.h @@ -0,0 +1,26 @@ +#pragma once + +#include "rendering/vertex_buffer.h" + +namespace Donut +{ + class VulkanVertexBuffer + : public VertexBuffer + { + public: + VulkanVertexBuffer(uint32_t size); + VulkanVertexBuffer(float* vertices, uint32_t size); + virtual ~VulkanVertexBuffer(); + + virtual auto bind() const -> void override; + virtual auto unbind() const -> void override; + + virtual auto set_data(const void* data, uint32_t size) -> void override; + + virtual auto get_layout() const -> const VertexBufferLayout& override{ return m_layout; } + virtual auto set_layout(const VertexBufferLayout& layout) -> void override{ m_layout = layout; } + private: + uint32_t m_renderer_id; + VertexBufferLayout m_layout; + }; +}; -- cgit v1.3