From 11ac5600095bb4fab96e1ff139b358226de4dd8f Mon Sep 17 00:00:00 2001 From: Jordan Arch Date: Tue, 17 Feb 2026 18:37:38 -0500 Subject: [PATCH] Triangle posting https://vulkan-tutorial.com/Vertex_buffers/Staging_buffer Reached the above part of the tutorial. Adding IMGUI now before continuing. --- .clangd | 5 +- Learning Vulkan.vcxproj | 6 +- Learning Vulkan.vcxproj.filters | 8 +- LearningVulkan | Bin 0 -> 277776 bytes Makefile | 24 +- Shaders/compile.sh | 3 + Shaders/shader.vert | 22 +- Shaders/vert.spv | Bin 1504 -> 1080 bytes src/VulkanContext.cpp | 1 - src/VulkanContext.h | 52 --- src/VulkanDeviceManager.cpp | 442 -------------------- src/VulkanDeviceManager.h | 101 ----- src/VulkanGraphicsPipeline.h | 29 -- src/VulkanInstanceManager.h | 67 --- src/main.cpp | 58 ++- src/{ => private}/GlfwWindowManager.cpp | 93 +--- src/private/VulkanCommandBuffers.cpp | 125 ++++++ src/private/VulkanContext.cpp | 287 +++++++++++++ src/{ => private}/VulkanDebugManager.cpp | 34 +- src/private/VulkanDeviceManager.cpp | 247 +++++++++++ src/private/VulkanFramebuffers.cpp | 48 +++ src/{ => private}/VulkanInstanceManager.cpp | 111 +---- src/private/VulkanPipeline.cpp | 275 ++++++++++++ src/private/VulkanRenderPass.cpp | 60 +++ src/private/VulkanSwapChain.cpp | 203 +++++++++ src/private/VulkanVertexBuffer.cpp | 74 ++++ src/{ => public}/GlfwWindowManager.h | 24 +- src/public/Primitives.h | 36 ++ src/public/VulkanCommandBuffers.h | 45 ++ src/public/VulkanContext.h | 77 ++++ src/{ => public}/VulkanDebugManager.h | 14 +- src/public/VulkanDeviceManager.h | 86 ++++ src/public/VulkanFramebuffers.h | 39 ++ src/public/VulkanInstanceManager.h | 27 ++ src/public/VulkanPipeline.h | 33 ++ src/public/VulkanRenderPass.h | 20 + src/public/VulkanSwapChain.h | 93 ++++ src/public/VulkanVertexBuffer.h | 35 ++ {include => src/utilities}/FileReader.h | 52 +-- {include => src/utilities}/Logger.cpp | 4 +- {include => src/utilities}/Logger.h | 0 41 files changed, 1961 insertions(+), 999 deletions(-) create mode 100755 LearningVulkan create mode 100755 Shaders/compile.sh delete mode 100644 src/VulkanContext.cpp delete mode 100644 src/VulkanContext.h delete mode 100644 src/VulkanDeviceManager.cpp delete mode 100644 src/VulkanDeviceManager.h delete mode 100644 src/VulkanGraphicsPipeline.h delete mode 100644 src/VulkanInstanceManager.h mode change 100644 => 100755 src/main.cpp rename src/{ => private}/GlfwWindowManager.cpp (55%) mode change 100644 => 100755 create mode 100644 src/private/VulkanCommandBuffers.cpp create mode 100755 src/private/VulkanContext.cpp rename src/{ => private}/VulkanDebugManager.cpp (70%) mode change 100644 => 100755 create mode 100755 src/private/VulkanDeviceManager.cpp create mode 100644 src/private/VulkanFramebuffers.cpp rename src/{ => private}/VulkanInstanceManager.cpp (51%) mode change 100644 => 100755 create mode 100755 src/private/VulkanPipeline.cpp create mode 100644 src/private/VulkanRenderPass.cpp create mode 100644 src/private/VulkanSwapChain.cpp create mode 100644 src/private/VulkanVertexBuffer.cpp rename src/{ => public}/GlfwWindowManager.h (66%) mode change 100644 => 100755 create mode 100644 src/public/Primitives.h create mode 100644 src/public/VulkanCommandBuffers.h create mode 100755 src/public/VulkanContext.h rename src/{ => public}/VulkanDebugManager.h (67%) mode change 100644 => 100755 create mode 100755 src/public/VulkanDeviceManager.h create mode 100644 src/public/VulkanFramebuffers.h create mode 100755 src/public/VulkanInstanceManager.h create mode 100755 src/public/VulkanPipeline.h create mode 100644 src/public/VulkanRenderPass.h create mode 100644 src/public/VulkanSwapChain.h create mode 100644 src/public/VulkanVertexBuffer.h rename {include => src/utilities}/FileReader.h (88%) mode change 100644 => 100755 rename {include => src/utilities}/Logger.cpp (98%) mode change 100644 => 100755 rename {include => src/utilities}/Logger.h (100%) mode change 100644 => 100755 diff --git a/.clangd b/.clangd index 028d859..0a8dc64 100644 --- a/.clangd +++ b/.clangd @@ -1,5 +1,6 @@ CompileFlags: - Add: + Add: - -I./include - - -I/usr/include/vulkan + - -I./src + - -I./src/public - -std=c++20 diff --git a/Learning Vulkan.vcxproj b/Learning Vulkan.vcxproj index 0a4340b..c1d6ef7 100644 --- a/Learning Vulkan.vcxproj +++ b/Learning Vulkan.vcxproj @@ -140,11 +140,11 @@ - + - + @@ -157,4 +157,4 @@ - \ No newline at end of file + diff --git a/Learning Vulkan.vcxproj.filters b/Learning Vulkan.vcxproj.filters index 888d47a..03a207b 100644 --- a/Learning Vulkan.vcxproj.filters +++ b/Learning Vulkan.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -41,7 +41,7 @@ - + Header Files @@ -59,7 +59,7 @@ Header Files - + Header Files @@ -75,4 +75,4 @@ Shaders - \ No newline at end of file + diff --git a/LearningVulkan b/LearningVulkan new file mode 100755 index 0000000000000000000000000000000000000000..5cc0525b9549ad5703d6080360a3a1a3c5fac06a GIT binary patch literal 277776 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

_ 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 literal 0 HcmV?d00001 diff --git a/Makefile b/Makefile index 0f42e59..7b1cb31 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,27 @@ CXX = g++ -CFLAGS = -std=c++20 -O2 -Wall -Wextra -I./include +CXXFLAGS = -std=c++20 -O2 -Wall -Wextra -I./include -I./src -I./src/private -I./src/public LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr -lXi -TARGET := LearningVulkan +BUILDDIR = build +OBJDIR = $(BUILDDIR)/obj -$(TARGET): ./src/main.cpp - $(CXX) $(CFLAGS) -o $(TARGET) ./src/main.cpp $(LDFLAGS) +SOURCES = $(shell find ./src -name "*.cpp") + +OBJECTS = $(SOURCES:./src/%.cpp=$(OBJDIR)/%.o) + +TARGET = LearningVulkan + +$(OBJDIR): + @mkdir -p $@ + +$(OBJDIR)/%.o: ./src/%.cpp | $(OBJDIR) + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +$(TARGET): $(OBJECTS) + $(CXX) $(OBJECTS) -o $@ $(LDFLAGS) .PHONY: test clean @@ -15,4 +29,4 @@ test: $(TARGET) ./$(TARGET) clean: - rm -f $(TARGET) + rm -rf $(BUILDDIR) diff --git a/Shaders/compile.sh b/Shaders/compile.sh new file mode 100755 index 0000000..5cd654c --- /dev/null +++ b/Shaders/compile.sh @@ -0,0 +1,3 @@ +#!/bin/bash +glslc ./shader.vert -o vert.spv +glslc ./shader.frag -o frag.spv diff --git a/Shaders/shader.vert b/Shaders/shader.vert index 66d6766..fa977f6 100644 --- a/Shaders/shader.vert +++ b/Shaders/shader.vert @@ -1,20 +1,12 @@ #version 450 +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + layout(location = 0) out vec3 fragColor; -vec2 positions[3] = vec2[]( - vec2(0.0, -0.5), - vec2(0.5, 0.5), - vec2(-0.5, 0.5) -); - -vec3 colors[3] = vec3[]( - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, 1.0) -); - void main() { - gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); - fragColor = colors[gl_VertexIndex]; -} \ No newline at end of file + gl_Position = vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} + diff --git a/Shaders/vert.spv b/Shaders/vert.spv index cf7123fbe1f034fcf872b733a735c8a4efd9c7f3..a8ec1a662f0a28d4e450f2e4d7681faee98ff26a 100644 GIT binary patch literal 1080 zcmYk4O;6iE5Qdi|CMhXUN+Gm-CF&6=;!vteRf|wS?jce@NL;NbF=!>@M2Kk<#Pvz z+cdwne>9nzA_iy0dWoH$+gUzLNAWzy28cZ*$vhV5p4^=i=wSyoosV=FJGsqIHVD>LnU~|0oSn+-Ma{u$FArEdo4;zeKg?hv3K*GU?&!W%w!l&=1 zSkSYhj`YFambEP1mb6r_pU3(1-;&>uZc8{vrOzKizdiXY($s?;$by^iK@4dt@2#}w z{=M|sEf{mwTwc+aIr!@m4!qt;GY8J}+mMC>IQ{V11sL_@evojyzvaB=Q|dp;W;R^u zzbj1*81cQ*2B$whyY+FuQy<4?*UbN_?+j0N$f4#{NFe<~D4aJX@Vyes-0c#Rnq#dJ uqvxfV!gWVNE-~)MozB#Q`+SnHQ}*^#Q}M}XCOq2`I1=M7_60nUc?u0MFnDF)EG3?WD_tXd~G(}N+zk@O66FYiu^P-@u*JZ3F?aDVc^abjOR}^*Rec7t&^?*uDZdsTY;mcUNHNK*l`ZeJhd1~N% zVfN;G@ULpjeZowB?r%b zYcjq?9UA(*E6?8CvAYBAl()eh+><8{x30*ScQxcN?q)-tnSB{^fj4Dv-1U~cFPo6D z4|rR~qW-=-Ie6;9J2glCpr4#8b{zD+OS{Tg%D)Ps4SXvn{ir=I+g-U4rsnOSK5 zit*&%2=S)4TVn2pb9*f{y*-f8*E^}{>5&XQJR17r{{#NBH0-%sb8=&DujZKfR8`FJ yM240e_srd~53@d%ah{wReSy)tzMd;~eKG5SjNakVF!!Mho_fv({7 -// #include -// -#include "VulkanInstanceManager.h" -#include "VulkanDebugManager.h" - -#define GLFW_INCLUDE_VULKAN -#include - -const std::vector ValidationLayers = { - "VK_LAYER_KHRONOS_validation" -}; - -struct FVulkanConfig -{ - bool bValidationEnabled = true; - bool bVerboseLogging = false; -}; - -class VulkanContext -{ -public: - VulkanContext(); - ~VulkanContext(); - - void Initialize(FVulkanConfig& Config); - void Cleanup(); - -private: - FVulkanConfig Config = {}; - - VkInstance Instance = VK_NULL_HANDLE; - VkPhysicalDevice PhysicalDevice = VK_NULL_HANDLE; - VkDevice Device = VK_NULL_HANDLE; - VkQueue GraphicsQueue = VK_NULL_HANDLE; - VkSurfaceKHR Surface = VK_NULL_HANDLE; - VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; - -public: - static VulkanDebugManager DebugManager; - -private: - VulkanInstanceManager InstanceManager; - -public: - VkInstance GetInstance() const { return Instance; } - VkPhysicalDevice GetPhysicalDevice() const { return PhysicalDevice; } - VkDevice GetDevice() const { return Device; } - VkQueue GetGraphicsQueue() const { return GraphicsQueue; } - VkSurfaceKHR GetSurface() const { return Surface; } -}; diff --git a/src/VulkanDeviceManager.cpp b/src/VulkanDeviceManager.cpp deleted file mode 100644 index 467bb1b..0000000 --- a/src/VulkanDeviceManager.cpp +++ /dev/null @@ -1,442 +0,0 @@ -#include "VulkanDeviceManager.h" - -#include -#include -#include // Necessary for uint32_t -#include // Necessary for std::numeric_limits -#include // Necessary for std::clamp - -#include "Logger.h" -#include "GlfwWindowManager.h" -#include - -std::vector VulkanDeviceManager::SwapChainImages = {}; - -VulkanDeviceManager::VulkanDeviceManager() -{ -} - -VulkanDeviceManager::~VulkanDeviceManager() -{ - // Cleanup(); -} - -VulkanDeviceManager::VulkanDeviceManager(VulkanDeviceManager&& Other) noexcept - : PhysicalDevice(std::exchange(Other.PhysicalDevice, VK_NULL_HANDLE)) - , Instance(std::exchange(Other.Instance, VK_NULL_HANDLE)) - , Device(std::exchange(Other.Device, VK_NULL_HANDLE)) - , GraphicsQueue(std::exchange(Other.GraphicsQueue, VK_NULL_HANDLE)) - , bEnableValidationLayers(std::exchange(Other.bEnableValidationLayers, false)) - , ValidationLayers(std::move(Other.ValidationLayers)) -{ -} - -VulkanDeviceManager& VulkanDeviceManager::operator=(VulkanDeviceManager&& Other) noexcept -{ - if (this != &Other) - { - Cleanup(); // Clean up current resources - - // Transfer resources from Other - PhysicalDevice = std::exchange(Other.PhysicalDevice, VK_NULL_HANDLE); - Instance = std::exchange(Other.Instance, VK_NULL_HANDLE); - Device = std::exchange(Other.Device, VK_NULL_HANDLE); - GraphicsQueue = std::exchange(Other.GraphicsQueue, VK_NULL_HANDLE); - bEnableValidationLayers = std::exchange(Other.bEnableValidationLayers, false); - ValidationLayers = std::move(Other.ValidationLayers); - } - return *this; -} - -void VulkanDeviceManager::Initialize( - VkInstance Instance, - bool bEnableValidationLayers, - const std::vector& ValidationLayers) -{ - if (IsInitialized()) - { - Log::Warning("Already Initialized."); - return; - } - - this->Instance = Instance; - this->bEnableValidationLayers = bEnableValidationLayers; - this->ValidationLayers = &ValidationLayers; - - PickPhysicalDevice(); - CreateLogicalDevice(); - CreateSwapChain(); - CreateImageViews(); -} - -void VulkanDeviceManager::Cleanup() -{ - if (!IsInitialized()) - { - Log::Warning("Not Initialized."); - return; - } - for (auto ImageView : SwapChainImageViews) - { - vkDestroyImageView(Device, ImageView, nullptr); - } - vkDestroySwapchainKHR(Device, SwapChain, nullptr); - - vkDestroySurfaceKHR(Instance, GlfwWindowManager::Surface, nullptr); - vkDestroyDevice(Device, nullptr); -} - -void VulkanDeviceManager::PickPhysicalDevice() -{ - uint32_t DeviceCount = 0; - vkEnumeratePhysicalDevices(Instance, &DeviceCount, nullptr); - - if (DeviceCount == 0) - { - Log::Error("Failed to find GPU with Vulkan Support."); - } - - std::vector Devices(DeviceCount); - vkEnumeratePhysicalDevices(Instance, &DeviceCount, Devices.data()); - - std::multimap Candidates; - - for (const auto& Device : Devices) - { - if (IsDeviceSuitable(Device)) - { - int Score = RateDeviceSuitability(Device); - Candidates.insert(std::make_pair(Score, Device)); - } - } - - if (Candidates.rbegin()->first > 0) - { - PhysicalDevice = Candidates.rbegin()->second; - Log::Info("Suitable GPU found."); - } - else - { - Log::Error("Failed to find a suitable GPU."); - } -} - -bool VulkanDeviceManager::IsDeviceSuitable(VkPhysicalDevice Device) -{ - QueueFamilyIndices Indices = FindQueueFamilies(Device); - - bool bExtensionsSupported = CheckDeviceExtensionSupport(Device); - - bool bSwapChainAdequate = false; - if (bExtensionsSupported) - { - SwapChainSupportDetails SwapChainSupport = QuerySwapChainSupport(Device); - bSwapChainAdequate = !SwapChainSupport.Formats.empty() && !SwapChainSupport.PresentModes.empty(); - } - - return Indices.IsComplete() && bExtensionsSupported && bSwapChainAdequate; -} - -int VulkanDeviceManager::RateDeviceSuitability(VkPhysicalDevice Device) -{ - VkPhysicalDeviceProperties DeviceProperties; - vkGetPhysicalDeviceProperties(Device, &DeviceProperties); - - VkPhysicalDeviceFeatures DeviceFeatures; - vkGetPhysicalDeviceFeatures(Device, &DeviceFeatures); - - int Score = 0; - - if (DeviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) - { - Score += 100; - } - - Score += DeviceProperties.limits.maxImageDimension2D; - - if (!DeviceFeatures.geometryShader) - { - return 0; - } - - return Score; -} - -bool VulkanDeviceManager::CheckDeviceExtensionSupport(VkPhysicalDevice Device) -{ - uint32_t ExtensionCount; - vkEnumerateDeviceExtensionProperties(Device, nullptr, &ExtensionCount, nullptr); - - std::vector AvailableExtensions(ExtensionCount); - vkEnumerateDeviceExtensionProperties(Device, nullptr, &ExtensionCount, AvailableExtensions.data()); - - std::set RequiredExtensions(DeviceExtensions.begin(), DeviceExtensions.end()); - - for (const auto& Extension : AvailableExtensions) - { - RequiredExtensions.erase(Extension.extensionName); - } - - return RequiredExtensions.empty(); -} - -QueueFamilyIndices VulkanDeviceManager::FindQueueFamilies(VkPhysicalDevice Device) -{ - QueueFamilyIndices Indices; - - uint32_t QueueFamilyCount = 0; - vkGetPhysicalDeviceQueueFamilyProperties(Device, &QueueFamilyCount, nullptr); - - std::vector QueueFamilies(QueueFamilyCount); - vkGetPhysicalDeviceQueueFamilyProperties(Device, &QueueFamilyCount, QueueFamilies.data()); - - int i = 0; - for (const auto& QueueFamily : QueueFamilies) - { - if (QueueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) - { - Indices.GraphicsFamily = i; - } - - VkBool32 PresentSupport = false; - vkGetPhysicalDeviceSurfaceSupportKHR(Device, i, GlfwWindowManager::Surface, &PresentSupport); - - if (PresentSupport) - { - Indices.PresentFamily = i; - } - if (Indices.IsComplete()) - { - break; - } - - i++; - } - - return Indices; -} - -void VulkanDeviceManager::CreateLogicalDevice() -{ - QueueFamilyIndices Indices = FindQueueFamilies(PhysicalDevice); - - std::vector QueueCreateInfos; - std::set UniqueQueueFamilies = { Indices.GraphicsFamily.value(), Indices.PresentFamily.value() }; - - float QueuePriority = 1.0f; - for (uint32_t QueueFamily : UniqueQueueFamilies) - { - VkDeviceQueueCreateInfo QueueCreateInfo{}; - QueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - QueueCreateInfo.queueFamilyIndex = Indices.GraphicsFamily.value(); - QueueCreateInfo.queueCount = 1; - QueueCreateInfo.pQueuePriorities = &QueuePriority; - QueueCreateInfos.push_back(QueueCreateInfo); - } - - VkPhysicalDeviceFeatures DeviceFeatures{}; - - VkDeviceCreateInfo CreateInfo{}; - CreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; - CreateInfo.queueCreateInfoCount = static_cast(QueueCreateInfos.size()); - CreateInfo.pQueueCreateInfos = QueueCreateInfos.data(); - CreateInfo.pEnabledFeatures = &DeviceFeatures; - - CreateInfo.enabledExtensionCount = static_cast(DeviceExtensions.size()); - CreateInfo.ppEnabledExtensionNames = DeviceExtensions.data(); - - if (bEnableValidationLayers) - { - CreateInfo.enabledLayerCount = static_cast(ValidationLayers->size()); - CreateInfo.ppEnabledLayerNames = ValidationLayers->data(); - } - else - { - CreateInfo.enabledLayerCount = 0; - } - - if (vkCreateDevice(PhysicalDevice, &CreateInfo, nullptr, &Device) != VK_SUCCESS) - { - Log::Error("Failed to create logical device!"); - } - - vkGetDeviceQueue(Device, Indices.GraphicsFamily.value(), 0, &GraphicsQueue); - vkGetDeviceQueue(Device, Indices.PresentFamily.value(), 0, &PresentQueue); -} - -SwapChainSupportDetails VulkanDeviceManager::QuerySwapChainSupport(VkPhysicalDevice Device) -{ - SwapChainSupportDetails Details; - - vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device, GlfwWindowManager::Surface, &Details.Capabilities); - - uint32_t FormatCount; - vkGetPhysicalDeviceSurfaceFormatsKHR(Device, GlfwWindowManager::Surface, &FormatCount, nullptr); - - if (FormatCount != 0) - { - Details.Formats.resize(FormatCount); - vkGetPhysicalDeviceSurfaceFormatsKHR(Device, GlfwWindowManager::Surface, &FormatCount, Details.Formats.data()); - } - - uint32_t PresentModeCount; - vkGetPhysicalDeviceSurfacePresentModesKHR(Device, GlfwWindowManager::Surface, &PresentModeCount, nullptr); - - if (PresentModeCount != 0) - { - Details.PresentModes.resize(PresentModeCount); - vkGetPhysicalDeviceSurfacePresentModesKHR(Device, GlfwWindowManager::Surface, &PresentModeCount, Details.PresentModes.data()); - } - - return Details; -} - -VkSurfaceFormatKHR VulkanDeviceManager::ChooseSwapSurfaceFormat(const std::vector& AvailableFormats) -{ - for (const auto& AvailableFormat : AvailableFormats) - { - if (AvailableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && AvailableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) - { - return AvailableFormat; - } - } - return AvailableFormats[0]; -} - -VkPresentModeKHR VulkanDeviceManager::ChooseSwapPresentMode(const std::vector& AvailablePresentModes) -{ - for (const auto& AvailablePresentMode : AvailablePresentModes) - { - if (AvailablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) - { - return AvailablePresentMode; - } - } - return VK_PRESENT_MODE_FIFO_KHR; -} - -VkExtent2D VulkanDeviceManager::ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& Capabilities) -{ - if (Capabilities.currentExtent.width != (std::numeric_limits::max)()) - { - return Capabilities.currentExtent; - } - else - { - int Width, Height; - glfwGetFramebufferSize(GlfwWindowManager::Window, &Width, &Height); - - VkExtent2D ActualExtent = { - static_cast(Width), - static_cast(Height) - }; - - ActualExtent.width = std::clamp(ActualExtent.width, Capabilities.minImageExtent.width, Capabilities.maxImageExtent.width); - ActualExtent.height = std::clamp(ActualExtent.height, Capabilities.minImageExtent.height, Capabilities.maxImageExtent.height); - - return ActualExtent; - } -} - -void VulkanDeviceManager::CreateSwapChain() -{ - SwapChainSupportDetails SwapChainSupport = QuerySwapChainSupport(PhysicalDevice); - - VkSurfaceFormatKHR SurfaceFormat = ChooseSwapSurfaceFormat(SwapChainSupport.Formats); - VkPresentModeKHR PresentMode = ChooseSwapPresentMode(SwapChainSupport.PresentModes); - VkExtent2D Extent = ChooseSwapExtent(SwapChainSupport.Capabilities); - - uint32_t ImageCount = SwapChainSupport.Capabilities.minImageCount + 1; - - if (SwapChainSupport.Capabilities.maxImageCount > 0 && ImageCount > SwapChainSupport.Capabilities.maxImageCount) - { - ImageCount = SwapChainSupport.Capabilities.maxImageCount; - } - - VkSwapchainCreateInfoKHR CreateInfo{}; - CreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; - CreateInfo.surface = GlfwWindowManager::Surface; - - CreateInfo.minImageCount = ImageCount; - CreateInfo.imageFormat = SurfaceFormat.format; - CreateInfo.imageColorSpace = SurfaceFormat.colorSpace; - CreateInfo.imageExtent = Extent; - CreateInfo.imageArrayLayers = 1; - CreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // may need VK_IMAGE_USAGE_TRANSFER_DST_BIT for post processing https://vulkan-tutorial.com/Drawing_a_triangle/Presentation/Swap_chain#:~:text=VK%5FIMAGE%5FUSAGE%5FTRANSFER%5FDST%5FBIT - - QueueFamilyIndices Indices = FindQueueFamilies(PhysicalDevice); - uint32_t QueueFamilyIndices[] = { Indices.GraphicsFamily.value(), - Indices.PresentFamily.value() }; - - if (Indices.GraphicsFamily != Indices.PresentFamily) - { - CreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; - CreateInfo.queueFamilyIndexCount = 2; - CreateInfo.pQueueFamilyIndices = QueueFamilyIndices; - } - else - { - CreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - CreateInfo.queueFamilyIndexCount = 0; - CreateInfo.pQueueFamilyIndices = nullptr; - } - - CreateInfo.preTransform = SwapChainSupport.Capabilities.currentTransform; - CreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - CreateInfo.presentMode = PresentMode; - CreateInfo.clipped = VK_TRUE; - CreateInfo.oldSwapchain = VK_NULL_HANDLE; - - if (vkCreateSwapchainKHR(Device, &CreateInfo, nullptr, &SwapChain) != VK_SUCCESS) - { - Log::Error("Failed to create swap chain."); - } - else - { - Log::Info("Successfully created swap chain."); - } - - vkGetSwapchainImagesKHR(Device, SwapChain, &ImageCount, nullptr); - SwapChainImages.resize(ImageCount); - vkGetSwapchainImagesKHR(Device, SwapChain, &ImageCount, SwapChainImages.data()); - - SwapChainImageFormat = SurfaceFormat.format; - SwapChainExtent = Extent; -} - -void VulkanDeviceManager::CreateImageViews() -{ - SwapChainImageViews.resize(SwapChainImages.size()); - - int CreatedViews = 0; - for (size_t i = 0; i < SwapChainImages.size(); i++) - { - VkImageViewCreateInfo CreateInfo{}; - CreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - CreateInfo.image = SwapChainImages[i]; - - CreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - CreateInfo.format = SwapChainImageFormat; - CreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; - CreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; - CreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; - CreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; - - CreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - CreateInfo.subresourceRange.baseMipLevel = 0; - CreateInfo.subresourceRange.levelCount = 1; - CreateInfo.subresourceRange.baseArrayLayer = 0; - CreateInfo.subresourceRange.layerCount = 1; - - if (vkCreateImageView(Device, &CreateInfo, nullptr, &SwapChainImageViews[i]) != VK_SUCCESS) - { - Log::Error("Failed to create image views."); - } - else - { - CreatedViews++; - } - } - - Log::Info("Successfully created " + std::to_string(CreatedViews) + " image views."); -} \ No newline at end of file diff --git a/src/VulkanDeviceManager.h b/src/VulkanDeviceManager.h deleted file mode 100644 index c47ac5a..0000000 --- a/src/VulkanDeviceManager.h +++ /dev/null @@ -1,101 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "Logger.h" - -#define GLFW_INCLUDE_VULKAN -#include - -const std::vector DeviceExtensions = { - VK_KHR_SWAPCHAIN_EXTENSION_NAME -}; - -struct QueueFamilyIndices -{ - std::optional GraphicsFamily; - std::optional PresentFamily; - - bool IsComplete() - { - return GraphicsFamily.has_value() && PresentFamily.has_value(); - } -}; - -struct SwapChainSupportDetails -{ - VkSurfaceCapabilitiesKHR Capabilities; - std::vector Formats; - std::vector PresentModes; -}; - -class VulkanDeviceManager -{ -public: - VulkanDeviceManager(); - ~VulkanDeviceManager(); - - VulkanDeviceManager(const VulkanDeviceManager&) = delete; - VulkanDeviceManager& operator=(const VulkanDeviceManager&) = delete; - - VulkanDeviceManager(VulkanDeviceManager&& Other) noexcept; - VulkanDeviceManager& operator=(VulkanDeviceManager&& Other) noexcept; - - void Initialize( - VkInstance Instance, - bool EnableValidationLayers, - const std::vector& ValidationLayers); - - void Cleanup(); - - bool IsInitialized() const - { - bool bInitialized = PhysicalDevice && Device && Instance && GraphicsQueue; - return bInitialized; - } - - static std::vector SwapChainImages; - -private: - VkPhysicalDevice PhysicalDevice = VK_NULL_HANDLE; - VkInstance Instance = VK_NULL_HANDLE; - VkDevice Device = VK_NULL_HANDLE; - VkQueue GraphicsQueue = VK_NULL_HANDLE; - VkQueue PresentQueue = VK_NULL_HANDLE; - - VkSwapchainKHR SwapChain = VK_NULL_HANDLE; - VkFormat SwapChainImageFormat; - VkExtent2D SwapChainExtent; - - std::vector SwapChainImageViews; - - bool bEnableValidationLayers = false; - - const std::vector* ValidationLayers = nullptr; - - void PickPhysicalDevice(); - - bool IsDeviceSuitable(VkPhysicalDevice Device); - - int RateDeviceSuitability(VkPhysicalDevice Device); - - bool CheckDeviceExtensionSupport(VkPhysicalDevice Device); - - QueueFamilyIndices FindQueueFamilies(VkPhysicalDevice Device); - - void CreateLogicalDevice(); - - SwapChainSupportDetails QuerySwapChainSupport(VkPhysicalDevice Device); - - VkSurfaceFormatKHR ChooseSwapSurfaceFormat(const std::vector& AvailableFormats); - - VkPresentModeKHR ChooseSwapPresentMode(const std::vector& AvailablePresentModes); - - VkExtent2D ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& Capabilities); - - void CreateSwapChain(); - - void CreateImageViews(); -}; diff --git a/src/VulkanGraphicsPipeline.h b/src/VulkanGraphicsPipeline.h deleted file mode 100644 index c98e9e7..0000000 --- a/src/VulkanGraphicsPipeline.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "FileReader.h" -#include "vulkan_core.h" - -class VulkanGraphicsPipeline -{ -public: - void CreateGraphicsPipeline() - { - auto VertShaderCode = ReadFile("Shaders/vert.spv"); - auto FragShaderCode = ReadFile("Shaders/frag.spv"); - - Log::Info("Vert buffer size: " + std::to_string(VertShaderCode.size())); - Log::Info("Frag buffer size: " + std::to_string(FragShaderCode.size())); - } - - VkShaderModule CreateShaderModule(const std::vector& Code) - { - VkShaderModuleCreateInfo CreateInfo{}; - CreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; - CreateInfo.codeSize = Code.size(); - CreateInfo.pCode = reinterpret_cast(Code.data()); - - VkShaderModule ShaderModule; - // if (vkCreateShaderModule(Device)) - return ShaderModule; - } - }; diff --git a/src/VulkanInstanceManager.h b/src/VulkanInstanceManager.h deleted file mode 100644 index 5aa9cc4..0000000 --- a/src/VulkanInstanceManager.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include - -#include "Logger.h" -#include "VulkanDebugManager.h" -#include "VulkanDeviceManager.h" -#include "GlfwWindowManager.h" - -#define GLFW_INCLUDE_VULKAN -#include - -const std::vector ValidationLayers = { - "VK_LAYER_KHRONOS_validation" -}; - -struct FVulkanConfig -{ - bool bValidationEnabled = true; - bool bVerboseLogging = false; -}; - -class VulkanInstanceManager -{ -public: - VulkanInstanceManager(); - ~VulkanInstanceManager(); - - VulkanInstanceManager(const VulkanInstanceManager&) = delete; - VulkanInstanceManager& operator=(const VulkanInstanceManager&) = delete; - - VulkanInstanceManager(VulkanInstanceManager&& Other) noexcept; - VulkanInstanceManager& operator=(VulkanInstanceManager&& Other) noexcept; - - // void Initialize(const FVulkanConfig& Config); - void Initialize(bool bEnableValidationLayers); - void SetupDebug(); - void SetupDevice(); - - // void Initialize(VulkanDebugManager& inDebugManager, bool inValidationEnabled); - void Cleanup(); - - bool IsInitialized() const { return Instance != VK_NULL_HANDLE; } - - const std::vector& GetValidationLayers() const - { - return ValidationLayers; - } - - const VkInstance GetInstance() const { return Instance; } - -private: - std::unique_ptr VkDebugManager = nullptr; - std::unique_ptr VkDeviceManager = nullptr; - - bool bValidationEnabled = false; - bool bVerboseLogging = false; - - VkInstance Instance = VK_NULL_HANDLE; - - std::vector GetRequiredExtensions(); - - bool CheckValidationLayerSupport(); - - void CreateInstance(); -}; diff --git a/src/main.cpp b/src/main.cpp old mode 100644 new mode 100755 index 326f4eb..f70db02 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,27 +1,27 @@ -#include "Logger.h" -#include "VulkanInstanceManager.h" -#include "VulkanDeviceManager.h" -#include "VulkanDebugManager.h" +#include +#include + +#include "utilities/Logger.h" #include "GlfwWindowManager.h" -#include "VulkanGraphicsPipeline.h" -// #include "VulkanContext.h" +#include "VulkanContext.h" struct AppConfig { std::string Title = "Learning Vulkan"; uint32_t Width = 800; uint32_t Height = 600; - bool bResizable = false; + bool bResizable = true; bool bFullscreen = false; - bool bValidationEnabled = true; + bool bValidationEnabled = false; bool bVerboseLogging = false; }; -void test () { - if (true) return; - -} +const std::vector TriangleVertices = { + { { 0.0f, -0.5f }, { 1.0f, 1.0f, 1.0f } }, + { { 0.5f, 0.5f }, { 0.0f, 1.0f, 0.0f } }, + { { -0.5f, 0.5f }, { 0.0f, 0.0f, 1.0f } } +}; class HelloTriangleApplication { @@ -29,6 +29,7 @@ public: void Run() { Initialization(); + Log::Info("Initialization finished..."); MainLoop(); Cleanup(); } @@ -36,25 +37,24 @@ public: private: AppConfig Settings = {}; - VulkanInstanceManager InstanceManager; - GlfwWindowManager WindowManager; + GlfwWindowManager WindowManager; + VulkanContext VkContext; void Initialization() { InitGlfw(); InitVulkan(); - InstanceManager.SetupDebug(); - WindowManager.CreateSurface(InstanceManager.GetInstance()); - InstanceManager.SetupDevice(); } void InitVulkan() { FVulkanConfig Config = { Settings.bValidationEnabled, - Settings.bVerboseLogging + Settings.bVerboseLogging, + WindowManager.GetWindow(), + TriangleVertices }; - InstanceManager.Initialize(Config); + VkContext.Initialize(Config); } void InitGlfw() @@ -67,6 +67,20 @@ private: Settings.bFullscreen }; WindowManager.Initialize(Config); + + glfwSetWindowUserPointer(WindowManager.GetWindow(), this); + glfwSetFramebufferSizeCallback(WindowManager.GetWindow(), FramebufferResizeCallback); + } + + static void FramebufferResizeCallback(GLFWwindow* Window, int Width, int Height) + { + auto App = reinterpret_cast(glfwGetWindowUserPointer(Window)); + App->VkContext.SetFramebufferResized(true); + } + + void DrawFrame() + { + VkContext.DrawFrame(); } void MainLoop() @@ -74,13 +88,15 @@ private: while (!WindowManager.ShouldClose()) { WindowManager.PollEvents(); + DrawFrame(); } } void Cleanup() { - WindowManager.Cleanup(InstanceManager.GetInstance()); - InstanceManager.Cleanup(); + Log::Info("Cleaning up..."); + VkContext.Cleanup(); + WindowManager.Cleanup(); } }; diff --git a/src/GlfwWindowManager.cpp b/src/private/GlfwWindowManager.cpp old mode 100644 new mode 100755 similarity index 55% rename from src/GlfwWindowManager.cpp rename to src/private/GlfwWindowManager.cpp index d4c1a26..b84b642 --- a/src/GlfwWindowManager.cpp +++ b/src/private/GlfwWindowManager.cpp @@ -1,41 +1,15 @@ #include "GlfwWindowManager.h" -#include +#include "utilities/Logger.h" -#include "Logger.h" - -VkSurfaceKHR GlfwWindowManager::Surface = VK_NULL_HANDLE; -GLFWwindow* GlfwWindowManager::Window = nullptr; - -GlfwWindowManager::GlfwWindowManager() = default; +GlfwWindowManager::GlfwWindowManager() +{ +} GlfwWindowManager::~GlfwWindowManager() { - // Cleanup(); } -// GlfwWindowManager::GlfwWindowManager(GlfwWindowManager&& Other) noexcept -// : Window(Other.Window), Config(Other.Config), Surface(Other.Surface) -//{ -// Other.Window = nullptr; -// } -// -// GlfwWindowManager& GlfwWindowManager::operator=(GlfwWindowManager&& Other) noexcept -//{ -// if (this != &Other) -// { -// Cleanup(); -// -// Window = Other.Window; -// Config = Other.Config; -// Surface = Other.Surface; -// -// Other.Window = nullptr; -// } -// -// return *this; -// } - void GlfwWindowManager::Initialize(const FWindowConfig& Config) { if (IsInitialized()) @@ -48,7 +22,7 @@ void GlfwWindowManager::Initialize(const FWindowConfig& Config) InitializeGlfw(); } -void GlfwWindowManager::Cleanup(VkInstance Instance) +void GlfwWindowManager::Cleanup() { if (!IsInitialized()) { @@ -92,46 +66,6 @@ void GlfwWindowManager::SetTitle(const std::string& Title) } } -void GlfwWindowManager::CreateSurface(VkInstance Instance) -{ - if (!Window) - { - Log::Error("Window not initialized."); - } - - if (!Instance) - { - Log::Error("Instance is null."); - } - - VkResult result = glfwCreateWindowSurface(Instance, Window, nullptr, &Surface); - if (result != VK_SUCCESS) - { - std::string errorMsg; - switch (result) - { - case VK_ERROR_EXTENSION_NOT_PRESENT: - errorMsg = "VK_ERROR_EXTENSION_NOT_PRESENT - Required extension not present"; - break; - case VK_ERROR_INITIALIZATION_FAILED: - errorMsg = "VK_ERROR_INITIALIZATION_FAILED - Initialization failed"; - break; - case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: - errorMsg = "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR - Native window already in use"; - break; - default: - errorMsg = "Unknown error code: " + std::to_string(result); - break; - } - - Log::Error("Failed to create window surface: " + errorMsg); - } - else - { - Log::Info("Window surface created successfully."); - } -} - void GlfwWindowManager::SetResizeCallback(GLFWwindowsizefun Callback) { if (Window) @@ -191,8 +125,16 @@ void GlfwWindowManager::InitializeGlfw() } } + glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); glfwWindowHint(GLFW_RESIZABLE, Config.bResizable ? GLFW_TRUE : GLFW_FALSE); + glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE); + glfwWindowHint(GLFW_DECORATED, GLFW_TRUE); + glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE); + glfwWindowHint(GLFW_AUTO_ICONIFY, GLFW_FALSE); + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + glfwWindowHintString(GLFW_WAYLAND_APP_ID, Config.Title.c_str()); Window = glfwCreateWindow( Config.Width, @@ -209,4 +151,13 @@ void GlfwWindowManager::InitializeGlfw() { Log::Info("Created GLFW window successfully."); } + + glfwSetWindowSize(Window, Config.Width, Config.Height); + const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); + if (mode) + { + int xPos = (mode->width - Config.Width) / 2; + int yPos = (mode->height - Config.Height) / 2; + glfwSetWindowPos(Window, xPos, yPos); + } } diff --git a/src/private/VulkanCommandBuffers.cpp b/src/private/VulkanCommandBuffers.cpp new file mode 100644 index 0000000..9368bca --- /dev/null +++ b/src/private/VulkanCommandBuffers.cpp @@ -0,0 +1,125 @@ +#include "VulkanCommandBuffers.h" + +#include "utilities/Logger.h" +#include +#include + +void VulkanCommandBuffers::Initialize( + VkDevice InDevice, + VkRenderPass InRenderPass) +{ + Device = InDevice; + RenderPass = InRenderPass; +} + +void VulkanCommandBuffers::Cleanup() +{ + vkDestroyCommandPool(Device, CommandPool, nullptr); +} + +void VulkanCommandBuffers::CreateCommandPool(std::optional GraphicsFamily) +{ + VkCommandPoolCreateInfo PoolInfo{}; + PoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + PoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + PoolInfo.queueFamilyIndex = GraphicsFamily.value(); + + if (vkCreateCommandPool(Device, &PoolInfo, nullptr, &CommandPool) != VK_SUCCESS) + { + Log::Error("Failed to create command pool!"); + } + else + { + Log::Info("Successfully created command pool"); + } +} + +void VulkanCommandBuffers::CreateCommandBuffers(int FramesInFlight) +{ + CommandBuffers.resize(FramesInFlight); + VkCommandBufferAllocateInfo AllocateInfo{}; + AllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + AllocateInfo.commandPool = CommandPool; + AllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + AllocateInfo.commandBufferCount = (uint32_t)CommandBuffers.size(); + + if (vkAllocateCommandBuffers(Device, &AllocateInfo, CommandBuffers.data()) != VK_SUCCESS) + { + Log::Error("Failed to allocate command buffers!"); + } + else + { + Log::Info("Successfully allocated command buffers."); + } +} + +void VulkanCommandBuffers::RecordCommandBuffer( + VkCommandBuffer InCommandBuffer, + uint32_t ImageIndex, + VkBuffer InVertexBuffer, + std::vector InVertices, + VkRenderPass RenderPass, + VkExtent2D SwapChainExtent, + VkPipeline GraphicsPipeline, + std::vector SwapChainFramebuffers) +{ + VkCommandBufferBeginInfo BeginInfo{}; + BeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + BeginInfo.flags = 0; + BeginInfo.pInheritanceInfo = nullptr; + + if (vkBeginCommandBuffer(InCommandBuffer, &BeginInfo) != VK_SUCCESS) + { + Log::Error("Failed to begin recording command buffer!"); + } + else + { + // Log::Info("Successfully began recording command buffers."); + } + + VkRenderPassBeginInfo RenderPassInfo{}; + RenderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + RenderPassInfo.renderPass = RenderPass; + RenderPassInfo.framebuffer = SwapChainFramebuffers[ImageIndex]; + RenderPassInfo.renderArea.offset = { 0, 0 }; + RenderPassInfo.renderArea.extent = { SwapChainExtent }; + + VkClearValue ClearColor = { { { 0.0f, 0.0f, 0.0f, 1.0f } } }; + RenderPassInfo.clearValueCount = 1; + RenderPassInfo.pClearValues = &ClearColor; + + vkCmdBeginRenderPass(InCommandBuffer, &RenderPassInfo, VK_SUBPASS_CONTENTS_INLINE); + + vkCmdBindPipeline(InCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, GraphicsPipeline); + + VkViewport Viewport{}; + Viewport.x = 0.0f; + Viewport.y = 0.0f; + Viewport.width = static_cast(SwapChainExtent.width); + Viewport.height = static_cast(SwapChainExtent.height); + Viewport.minDepth = 0.0f; + Viewport.maxDepth = 1.0f; + vkCmdSetViewport(InCommandBuffer, 0, 1, &Viewport); + + VkRect2D Scissor{}; + Scissor.offset = { 0, 0 }; + Scissor.extent = SwapChainExtent; + vkCmdSetScissor(InCommandBuffer, 0, 1, &Scissor); + + VkBuffer VertexBuffers[] = { InVertexBuffer }; + VkDeviceSize Offsets[] = { 0 }; + vkCmdBindVertexBuffers(InCommandBuffer, 0, 1, VertexBuffers, Offsets); + + vkCmdDraw(InCommandBuffer, static_cast(InVertices.size()), 1, 0, 0); + + vkCmdEndRenderPass(InCommandBuffer); + + if (vkEndCommandBuffer(InCommandBuffer) != VK_SUCCESS) + { + Log::Error("Failed to record command buffer!"); + } + else + { + // Log::Info("Successfully recorded command buffer."); + } +} diff --git a/src/private/VulkanContext.cpp b/src/private/VulkanContext.cpp new file mode 100755 index 0000000..fa73a86 --- /dev/null +++ b/src/private/VulkanContext.cpp @@ -0,0 +1,287 @@ +#include "VulkanContext.h" +#include "VulkanDeviceManager.h" +#include "VulkanFramebuffers.h" +#include "VulkanSwapChain.h" +#include "VulkanVertexBuffer.h" +#include "utilities/Logger.h" +#include +#include +#include + +VulkanContext::VulkanContext() +{ +} + +VulkanContext::~VulkanContext() +{ +} + +void VulkanContext::Initialize(FVulkanConfig& InConfig) +{ + Config = InConfig; + + if (Config.bValidationEnabled) + { + InstanceManager.CreateInstance(&DebugManager); + DebugManager.Initialize(InstanceManager.GetInstance()); + } + else + { + InstanceManager.CreateInstance(); + } + + CreateSurface(InConfig.Window); + + DeviceManager.Initialize(FDeviceConfig( + InstanceManager.GetInstance(), + Config.bValidationEnabled, + Surface, + InConfig.Window)); + DeviceManager.PickPhysicalDevice(); + DeviceManager.CreateLogicalDevice(); + + SwapChain.Initialize(FSwapConfig( + DeviceManager.GetDevice(), + Surface, + Config.Window, + DeviceManager.GetPhysicalQueueFamilies().GraphicsFamily, + DeviceManager.GetPhysicalQueueFamilies().PresentFamily, + DeviceManager.GetSwapChainSupport().Capabilities, + DeviceManager.GetSwapChainSupport().Formats, + DeviceManager.GetSwapChainSupport().PresentModes)); + SwapChain.CreateSwapChain(); + SwapChain.CreateImageViews(); + + RenderPass.Initialize(DeviceManager.GetDevice()); + RenderPass.CreateRenderPass(SwapChain.GetSwapChainImageFormat()); + + GraphicsPipeline.Initialize(DeviceManager.GetDevice()); + GraphicsPipeline.CreateGraphicsPipeline(SwapChain.GetSwapChainExtent(), RenderPass.GetRenderPass()); + + Framebuffers.Initialize(FFramebufferConfig( + DeviceManager.GetDevice(), + RenderPass.GetRenderPass(), + SwapChain.GetSwapChainImageViews(), + SwapChain.GetSwapChainExtent())); + Framebuffers.CreateFramebuffers(); + + CommandBuffers.Initialize(DeviceManager.GetDevice(), RenderPass.GetRenderPass()); + CommandBuffers.CreateCommandPool(DeviceManager.GetPhysicalQueueFamilies().GraphicsFamily); + + VertexBuffer.Initialize(FVertexBufferConfig(DeviceManager.GetDevice(), DeviceManager.GetPhysicalDevice())); + VertexBuffer.CreateVertexBuffer(Config.Vertices); + + CommandBuffers.CreateCommandBuffers(MAX_FRAMES_IN_FLIGHT); + + CreateSyncObjects(); +} + +void VulkanContext::Cleanup() +{ + CleanupSwapChain(); + + VertexBuffer.Cleanup(); + + GraphicsPipeline.Cleanup(); + RenderPass.Cleanup(); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + vkDestroySemaphore(DeviceManager.GetDevice(), ImageAvailableSemaphores[i], nullptr); + vkDestroySemaphore(DeviceManager.GetDevice(), RenderFinishedSemaphores[i], nullptr); + vkDestroyFence(DeviceManager.GetDevice(), InFlightFences[i], nullptr); + } + + CommandBuffers.Cleanup(); + + DeviceManager.Cleanup(); + + if (Config.bValidationEnabled) + { + DebugManager.Cleanup(); + } + + vkDestroySurfaceKHR(InstanceManager.GetInstance(), Surface, nullptr); + InstanceManager.Cleanup(); +} + +void VulkanContext::CreateSurface(GLFWwindow* Window) +{ + if (!Window) + { + Log::Error("Window not initialized."); + } + + if (!InstanceManager.GetInstance()) + { + Log::Error("Instance is null."); + } + + VkResult result = glfwCreateWindowSurface(InstanceManager.GetInstance(), Window, nullptr, &Surface); + if (result != VK_SUCCESS) + { + std::string errorMsg; + switch (result) + { + case VK_ERROR_EXTENSION_NOT_PRESENT: + errorMsg = "VK_ERROR_EXTENSION_NOT_PRESENT - Required extension not present"; + break; + case VK_ERROR_INITIALIZATION_FAILED: + errorMsg = "VK_ERROR_INITIALIZATION_FAILED - Initialization failed"; + break; + case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: + errorMsg = "VK_ERROR_NATIVE_WINDOW_IN_USE_KHR - Native window already in use"; + break; + default: + errorMsg = "Unknown error code: " + std::to_string(result); + break; + } + + Log::Error("Failed to create window surface: " + errorMsg); + } + else + { + Log::Info("Window surface created successfully."); + } +} + +void VulkanContext::CreateSyncObjects() +{ + ImageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + RenderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + InFlightFences.resize(MAX_FRAMES_IN_FLIGHT); + + VkSemaphoreCreateInfo SemaphoreInfo{}; + SemaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo FenceInfo{}; + FenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + FenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + if (vkCreateSemaphore(DeviceManager.GetDevice(), &SemaphoreInfo, nullptr, &ImageAvailableSemaphores[i]) != VK_SUCCESS + || vkCreateSemaphore(DeviceManager.GetDevice(), &SemaphoreInfo, nullptr, &RenderFinishedSemaphores[i]) != VK_SUCCESS + || vkCreateFence(DeviceManager.GetDevice(), &FenceInfo, nullptr, &InFlightFences[i]) != VK_SUCCESS) + { + Log::Error("Failed to create semaphores!"); + } + else + { + Log::Info("Successfully created semaphores"); + } + } +} + +void VulkanContext::DrawFrame() +{ + vkWaitForFences(DeviceManager.GetDevice(), 1, &InFlightFences[CurrentFrame], VK_TRUE, UINT64_MAX); + + uint32_t ImageIndex; + VkResult result = vkAcquireNextImageKHR( + DeviceManager.GetDevice(), + SwapChain.GetSwapChain(), + UINT64_MAX, + ImageAvailableSemaphores[CurrentFrame], + VK_NULL_HANDLE, + &ImageIndex); + + if (result == VK_ERROR_OUT_OF_DATE_KHR) + { + RecreateSwapChain(); + return; + } + else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) + { + Log::Error("Failed to acquire swap chain images!"); + } + + vkResetFences(DeviceManager.GetDevice(), 1, &InFlightFences[CurrentFrame]); + + vkAcquireNextImageKHR(DeviceManager.GetDevice(), SwapChain.GetSwapChain(), UINT64_MAX, ImageAvailableSemaphores[CurrentFrame], VK_NULL_HANDLE, &ImageIndex); + + vkResetCommandBuffer(CommandBuffers.GetCommandBuffer(CurrentFrame), 0); + CommandBuffers.RecordCommandBuffer( + CommandBuffers.GetCommandBuffer(CurrentFrame), + ImageIndex, + VertexBuffer.GetVertexBuffer(), + Config.Vertices, + RenderPass.GetRenderPass(), + SwapChain.GetSwapChainExtent(), + GraphicsPipeline.GetGraphicsPipeline(), + Framebuffers.GetSwapChainFrameBuffers()); + + VkSubmitInfo SubmitInfo{}; + SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + VkSemaphore WaitSemaphores[] = { ImageAvailableSemaphores[CurrentFrame] }; + VkPipelineStageFlags WaitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; + SubmitInfo.waitSemaphoreCount = 1; + SubmitInfo.pWaitSemaphores = WaitSemaphores; + SubmitInfo.pWaitDstStageMask = WaitStages; + + SubmitInfo.commandBufferCount = 1; + VkCommandBuffer CommandBuffer = CommandBuffers.GetCommandBuffer(CurrentFrame); + SubmitInfo.pCommandBuffers = &CommandBuffer; + + VkSemaphore SignalSemaphores[] = { RenderFinishedSemaphores[CurrentFrame] }; + SubmitInfo.signalSemaphoreCount = 1; + SubmitInfo.pSignalSemaphores = SignalSemaphores; + + if (vkQueueSubmit(DeviceManager.GetGraphicsQueue(), 1, &SubmitInfo, InFlightFences[CurrentFrame]) != VK_SUCCESS) + { + Log::Error("Failed to submit draw command buffer!"); + } + + VkPresentInfoKHR PresentInfo{}; + PresentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + PresentInfo.waitSemaphoreCount = 1; + PresentInfo.pWaitSemaphores = SignalSemaphores; + + VkSwapchainKHR SwapChains[] = { SwapChain.GetSwapChain() }; + + PresentInfo.swapchainCount = 1; + PresentInfo.pSwapchains = SwapChains; + PresentInfo.pImageIndices = &ImageIndex; + PresentInfo.pResults = nullptr; + + result = vkQueuePresentKHR(DeviceManager.GetPresentQueue(), &PresentInfo); + + if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || bFramebufferResized) + { + bFramebufferResized = false; + RecreateSwapChain(); + } + else if (result != VK_SUCCESS) + { + Log::Error("Failed to present swap chain image!"); + } + + CurrentFrame = (CurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} + +void VulkanContext::RecreateSwapChain() +{ + int Width = 0, Height = 0; + glfwGetFramebufferSize(Config.Window, &Width, &Height); + while (Width == 0 || Height == 0) + { + glfwGetFramebufferSize(Config.Window, &Width, &Height); + glfwWaitEvents(); + } + + Log::Info("Recreating SwapChain..."); + vkDeviceWaitIdle(DeviceManager.GetDevice()); + + CleanupSwapChain(); + + SwapChain.CreateSwapChain(); + SwapChain.CreateImageViews(); + Framebuffers.CreateFramebuffers(); +} + +void VulkanContext::CleanupSwapChain() +{ + Framebuffers.Cleanup(); + SwapChain.Cleanup(); +} diff --git a/src/VulkanDebugManager.cpp b/src/private/VulkanDebugManager.cpp old mode 100644 new mode 100755 similarity index 70% rename from src/VulkanDebugManager.cpp rename to src/private/VulkanDebugManager.cpp index 4ccd94c..0cfd9ae --- a/src/VulkanDebugManager.cpp +++ b/src/private/VulkanDebugManager.cpp @@ -1,5 +1,5 @@ #include "VulkanDebugManager.h" -#include "Logger.h" +#include "utilities/Logger.h" VulkanDebugManager::VulkanDebugManager() { @@ -7,28 +7,6 @@ VulkanDebugManager::VulkanDebugManager() VulkanDebugManager::~VulkanDebugManager() { - // Cleanup(); -} - -VulkanDebugManager::VulkanDebugManager(VulkanDebugManager&& Other) noexcept - : DebugMessenger(Other.DebugMessenger), Instance(Other.Instance) -{ - Other.DebugMessenger = VK_NULL_HANDLE; - Other.Instance = VK_NULL_HANDLE; -} - -VulkanDebugManager& VulkanDebugManager::operator=(VulkanDebugManager&& Other) noexcept -{ - if (this != &Other) - { - Cleanup(); - DebugMessenger = Other.DebugMessenger; - Instance = Other.Instance; - Other.DebugMessenger = VK_NULL_HANDLE; - Other.Instance = VK_NULL_HANDLE; - } - - return *this; } void VulkanDebugManager::Initialize(VkInstance Instance) @@ -49,12 +27,12 @@ void VulkanDebugManager::Cleanup() return; } - FDestroyDebugUtilsMessengerExtParams Params = { Instance, DebugMessenger, nullptr }; + FDestroyParams Params = { Instance, DebugMessenger, nullptr }; DestroyDebugUtilsMessengerExt(Params); DebugMessenger = VK_NULL_HANDLE; } -VkResult VulkanDebugManager::CreateDebugUtilsMessengerExt(const FCreateDebugUtilsMessengerExtParams& Params) +VkResult VulkanDebugManager::CreateDebugUtilsMessengerExt(const FCreateParams& Params) { auto func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(Params.Instance, "vkCreateDebugUtilsMessengerEXT"); if (func != nullptr) @@ -67,7 +45,7 @@ VkResult VulkanDebugManager::CreateDebugUtilsMessengerExt(const FCreateDebugUtil } } -void VulkanDebugManager::DestroyDebugUtilsMessengerExt(const FDestroyDebugUtilsMessengerExtParams& Params) +void VulkanDebugManager::DestroyDebugUtilsMessengerExt(const FDestroyParams& Params) { auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr( Params.Instance, @@ -86,7 +64,6 @@ VKAPI_ATTR VkBool32 VKAPI_CALL VulkanDebugManager::DebugCallback( void* pUserData) { Log::Validation(pCallbackData->pMessage); - // std::cerr << "[Validation layer] : " << pCallbackData->pMessage << std::endl; if (MessageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { @@ -110,11 +87,10 @@ void VulkanDebugManager::SetupDebugMessanger() PopulateDebugMessengerCreateInfo(CreateInfo); - FCreateDebugUtilsMessengerExtParams Params = { Instance, &CreateInfo, nullptr, &DebugMessenger }; + FCreateParams Params = { Instance, &CreateInfo, nullptr, &DebugMessenger }; if (CreateDebugUtilsMessengerExt(Params) != VK_SUCCESS) { Log::Error("Failed to set up debug messenger!"); - // throw std::runtime_error("failed to set up debug messenger!"); } } diff --git a/src/private/VulkanDeviceManager.cpp b/src/private/VulkanDeviceManager.cpp new file mode 100755 index 0000000..e989b81 --- /dev/null +++ b/src/private/VulkanDeviceManager.cpp @@ -0,0 +1,247 @@ +#include "VulkanDeviceManager.h" + +#include +#include +#include // Necessary for uint32_t +#include +#include + +#include "VulkanContext.h" +#include "utilities/Logger.h" + +VulkanDeviceManager::VulkanDeviceManager() +{ +} + +VulkanDeviceManager::~VulkanDeviceManager() +{ + // Cleanup(); +} + +void VulkanDeviceManager::Initialize(FDeviceConfig InConfig) +// VkInstance Instance, +// bool bEnableValidationLayers, +// VkSurfaceKHR Surface, +// GLFWwindow* Window) +{ + DeviceConfig = InConfig; + // this->Instance = Instance; + // this->Surface = Surface; + // this->bEnableValidationLayers = bEnableValidationLayers; + // this->Window = Window; +} + +void VulkanDeviceManager::Cleanup() +{ + vkDestroyDevice(Device, nullptr); +} + +void VulkanDeviceManager::PickPhysicalDevice() +{ + uint32_t DeviceCount = 0; + vkEnumeratePhysicalDevices(DeviceConfig.Instance, &DeviceCount, nullptr); + + if (DeviceCount == 0) + { + Log::Error("Failed to find GPU with Vulkan Support."); + } + + std::vector Devices(DeviceCount); + vkEnumeratePhysicalDevices(DeviceConfig.Instance, &DeviceCount, Devices.data()); + + std::multimap Candidates; + + for (const auto& Device : Devices) + { + if (IsDeviceSuitable(Device)) + { + int Score = RateDeviceSuitability(Device); + Candidates.insert(std::make_pair(Score, Device)); + } + } + + if (Candidates.rbegin()->first > 0) + { + PhysicalDevice = Candidates.rbegin()->second; + SwapChainSupport = QuerySwapChainSupport(PhysicalDevice); + Log::Info("Suitable GPU found."); + } + else + { + Log::Error("Failed to find a suitable GPU."); + } +} + +bool VulkanDeviceManager::IsDeviceSuitable(VkPhysicalDevice Device) +{ + QueueFamilyIndices Indices = FindQueueFamilies(Device); + + bool bExtensionsSupported = CheckDeviceExtensionSupport(Device); + + bool bSwapChainAdequate = false; + if (bExtensionsSupported) + { + SwapChainSupport = QuerySwapChainSupport(Device); + bSwapChainAdequate = !SwapChainSupport.Formats.empty() && !SwapChainSupport.PresentModes.empty(); + } + + return Indices.IsComplete() && bExtensionsSupported && bSwapChainAdequate; +} + +int VulkanDeviceManager::RateDeviceSuitability(VkPhysicalDevice Device) +{ + VkPhysicalDeviceProperties DeviceProperties; + vkGetPhysicalDeviceProperties(Device, &DeviceProperties); + + VkPhysicalDeviceFeatures DeviceFeatures; + vkGetPhysicalDeviceFeatures(Device, &DeviceFeatures); + + int Score = 0; + + if (DeviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) + { + Score += 100; + } + + Score += DeviceProperties.limits.maxImageDimension2D; + + if (!DeviceFeatures.geometryShader) + { + return 0; + } + + return Score; +} + +bool VulkanDeviceManager::CheckDeviceExtensionSupport(VkPhysicalDevice Device) +{ + uint32_t ExtensionCount; + vkEnumerateDeviceExtensionProperties(Device, nullptr, &ExtensionCount, nullptr); + + std::vector AvailableExtensions(ExtensionCount); + vkEnumerateDeviceExtensionProperties(Device, nullptr, &ExtensionCount, AvailableExtensions.data()); + + std::set RequiredExtensions(DeviceExtensions.begin(), DeviceExtensions.end()); + + for (const auto& Extension : AvailableExtensions) + { + RequiredExtensions.erase(Extension.extensionName); + } + + return RequiredExtensions.empty(); +} + +QueueFamilyIndices VulkanDeviceManager::FindQueueFamilies(VkPhysicalDevice Device) +{ + QueueFamilyIndices Indices; + + uint32_t QueueFamilyCount = 0; + vkGetPhysicalDeviceQueueFamilyProperties(Device, &QueueFamilyCount, nullptr); + + std::vector QueueFamilies(QueueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(Device, &QueueFamilyCount, QueueFamilies.data()); + + int i = 0; + for (const auto& QueueFamily : QueueFamilies) + { + if (QueueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) + { + Indices.GraphicsFamily = i; + } + + VkBool32 PresentSupport = false; + vkGetPhysicalDeviceSurfaceSupportKHR(Device, i, DeviceConfig.Surface, &PresentSupport); + + if (PresentSupport) + { + Indices.PresentFamily = i; + } + if (Indices.IsComplete()) + { + break; + } + + i++; + } + + return Indices; +} + +void VulkanDeviceManager::CreateLogicalDevice() +{ + PhysicalQueueFamilies = FindQueueFamilies(PhysicalDevice); + + std::vector QueueCreateInfos; + std::set UniqueQueueFamilies = { PhysicalQueueFamilies.GraphicsFamily.value(), PhysicalQueueFamilies.PresentFamily.value() }; + + float QueuePriority = 1.0f; + for (uint32_t QueueFamily : UniqueQueueFamilies) + { + VkDeviceQueueCreateInfo QueueCreateInfo{}; + QueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + QueueCreateInfo.queueFamilyIndex = PhysicalQueueFamilies.GraphicsFamily.value(); + QueueCreateInfo.queueCount = 1; + QueueCreateInfo.pQueuePriorities = &QueuePriority; + QueueCreateInfos.push_back(QueueCreateInfo); + } + + VkPhysicalDeviceFeatures DeviceFeatures{}; + + VkDeviceCreateInfo CreateInfo{}; + CreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + CreateInfo.queueCreateInfoCount = static_cast(QueueCreateInfos.size()); + CreateInfo.pQueueCreateInfos = QueueCreateInfos.data(); + CreateInfo.pEnabledFeatures = &DeviceFeatures; + + CreateInfo.enabledExtensionCount = static_cast(DeviceExtensions.size()); + CreateInfo.ppEnabledExtensionNames = DeviceExtensions.data(); + + if (DeviceConfig.bEnableValidationLayers) + { + CreateInfo.enabledLayerCount = static_cast(ValidationLayers.size()); + CreateInfo.ppEnabledLayerNames = ValidationLayers.data(); + } + else + { + CreateInfo.enabledLayerCount = 0; + } + + if (vkCreateDevice(PhysicalDevice, &CreateInfo, nullptr, &Device) != VK_SUCCESS) + { + Log::Error("Failed to create logical device!"); + } + else + { + Log::Info("Created logical device."); + } + + vkGetDeviceQueue(Device, PhysicalQueueFamilies.GraphicsFamily.value(), 0, &GraphicsQueue); + vkGetDeviceQueue(Device, PhysicalQueueFamilies.PresentFamily.value(), 0, &PresentQueue); +} + +SwapChainSupportDetails VulkanDeviceManager::QuerySwapChainSupport(VkPhysicalDevice Device) +{ + SwapChainSupportDetails Details; + + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device, DeviceConfig.Surface, &Details.Capabilities); + + uint32_t FormatCount; + vkGetPhysicalDeviceSurfaceFormatsKHR(Device, DeviceConfig.Surface, &FormatCount, nullptr); + + if (FormatCount != 0) + { + Details.Formats.resize(FormatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(Device, DeviceConfig.Surface, &FormatCount, Details.Formats.data()); + } + + uint32_t PresentModeCount; + vkGetPhysicalDeviceSurfacePresentModesKHR(Device, DeviceConfig.Surface, &PresentModeCount, nullptr); + + if (PresentModeCount != 0) + { + Details.PresentModes.resize(PresentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(Device, DeviceConfig.Surface, &PresentModeCount, Details.PresentModes.data()); + } + + return Details; +} diff --git a/src/private/VulkanFramebuffers.cpp b/src/private/VulkanFramebuffers.cpp new file mode 100644 index 0000000..24c4efe --- /dev/null +++ b/src/private/VulkanFramebuffers.cpp @@ -0,0 +1,48 @@ +#include "VulkanFramebuffers.h" + +#include "utilities/Logger.h" +#include + +void VulkanFramebuffers::Initialize(FFramebufferConfig InConfig) +{ + FramebufferConfig = InConfig; +} + +void VulkanFramebuffers::Cleanup() +{ + for (auto Framebuffer : SwapChainFramebuffers) + { + vkDestroyFramebuffer(FramebufferConfig.Device, Framebuffer, nullptr); + } +} + +// void VulkanFramebuffers::CreateFramebuffers(VkRenderPass RenderPass, std::vector SwapChainImageViews, VkExtent2D SwapChainExtent) +void VulkanFramebuffers::CreateFramebuffers() +{ + SwapChainFramebuffers.resize(FramebufferConfig.SwapChainImageViews.size()); + + for (size_t i = 0; i < FramebufferConfig.SwapChainImageViews.size(); i++) + { + VkImageView Attachments[] = { + FramebufferConfig.SwapChainImageViews[i] + }; + + VkFramebufferCreateInfo FramebufferInfo{}; + FramebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + FramebufferInfo.renderPass = FramebufferConfig.RenderPass; + FramebufferInfo.attachmentCount = 1; + FramebufferInfo.pAttachments = Attachments; + FramebufferInfo.width = FramebufferConfig.SwapChainExtent.width; + FramebufferInfo.height = FramebufferConfig.SwapChainExtent.height; + FramebufferInfo.layers = 1; + + if (vkCreateFramebuffer(FramebufferConfig.Device, &FramebufferInfo, nullptr, &SwapChainFramebuffers[i]) != VK_SUCCESS) + { + Log::Error("Failed to create frame buffer!"); + } + else + { + Log::Info("Successfully created framebuffers."); + } + } +} diff --git a/src/VulkanInstanceManager.cpp b/src/private/VulkanInstanceManager.cpp old mode 100644 new mode 100755 similarity index 51% rename from src/VulkanInstanceManager.cpp rename to src/private/VulkanInstanceManager.cpp index 1413094..c47ba16 --- a/src/VulkanInstanceManager.cpp +++ b/src/private/VulkanInstanceManager.cpp @@ -1,6 +1,8 @@ #include "VulkanInstanceManager.h" -#include "Logger.h" +#include "VulkanContext.h" +#include "utilities/Logger.h" +#include VulkanInstanceManager::VulkanInstanceManager() { @@ -8,107 +10,14 @@ VulkanInstanceManager::VulkanInstanceManager() VulkanInstanceManager::~VulkanInstanceManager() { - // Cleanup(); -} - -VulkanInstanceManager::VulkanInstanceManager(VulkanInstanceManager&& Other) noexcept - : Instance(Other.Instance) /*, bValidationEnabled(Other.bValidationEnabled), bVerboseLogging(Other.bVerboseLogging), VkDebugManager(std::move(Other.VkDebugManager))*/ -{ - Other.Instance = VK_NULL_HANDLE; - Other.bValidationEnabled = false; - Other.bVerboseLogging = false; -} - -VulkanInstanceManager& VulkanInstanceManager::operator=(VulkanInstanceManager&& Other) noexcept -{ - if (this != &Other) - { - Cleanup(); - Instance = Other.Instance; - bValidationEnabled = Other.bValidationEnabled; - bVerboseLogging = Other.bVerboseLogging; - VkDebugManager = std::move(Other.VkDebugManager); - - Other.Instance = VK_NULL_HANDLE; - } - - return *this; -} - -// void VulkanInstanceManager::Initialize(const FVulkanConfig& Config) -//{ -// if (IsInitialized()) -// { -// Log::Warning("Already Initialized."); -// return; -// } -// -// bValidationEnabled = Config.bValidationEnabled; -// bVerboseLogging = Config.bVerboseLogging; -// -// if (bValidationEnabled) -// { -// Log::Info("DebugManager created with validation enabled."); -// VkDebugManager = std::make_unique(); -// } -// -// CreateInstance(); -// } - -void VulkanInstanceManager::Initialize(bool bEnableValidationLayers) -{ - if (IsInitialized()) - { - Log::Warning("Already Initialized."); - return; - } - - if (bEnableValidationLayers) - { - Log::Info("DebugManager created with validation enabled."); - VkDebugManager = std::make_unique(); - } - - CreateInstance(); -} - -void VulkanInstanceManager::SetupDebug() -{ - if (bValidationEnabled) - { - VkDebugManager->Initialize(Instance); - } -} - -void VulkanInstanceManager::SetupDevice() -{ - VkDeviceManager = std::make_unique(); - VkDeviceManager->Initialize(Instance, bValidationEnabled, ValidationLayers); } void VulkanInstanceManager::Cleanup() { - if (!IsInitialized()) - { - Log::Warning("Not initialized."); - return; - } - - if (VkDeviceManager) - { - VkDeviceManager->Cleanup(); - } - - if (VkDebugManager) - { - VkDebugManager->Cleanup(); - } - vkDestroyInstance(Instance, nullptr); - Instance = VK_NULL_HANDLE; } -std::vector VulkanInstanceManager::GetRequiredExtensions() +std::vector VulkanInstanceManager::GetRequiredExtensions(bool bEnableValidationLayers) { uint32_t GlfwExtensionCount = 0; const char** GlfwExtensions; @@ -116,7 +25,7 @@ std::vector VulkanInstanceManager::GetRequiredExtensions() std::vector Extensions(GlfwExtensions, GlfwExtensions + GlfwExtensionCount); - if (VkDebugManager) + if (bEnableValidationLayers) { Extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } @@ -154,11 +63,11 @@ bool VulkanInstanceManager::CheckValidationLayerSupport() return true; } -void VulkanInstanceManager::CreateInstance() +void VulkanInstanceManager::CreateInstance(VulkanDebugManager* DebugManager) { Log::Info("Creating Vulkan instance."); - if (bValidationEnabled && !CheckValidationLayerSupport()) + if (DebugManager && !CheckValidationLayerSupport()) { Log::Error("Validation layers requested, but not available!"); } @@ -175,17 +84,17 @@ void VulkanInstanceManager::CreateInstance() CreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; CreateInfo.pApplicationInfo = &AppInfo; - auto Extensions = GetRequiredExtensions(); + auto Extensions = GetRequiredExtensions(DebugManager != nullptr); CreateInfo.enabledExtensionCount = static_cast(Extensions.size()); CreateInfo.ppEnabledExtensionNames = Extensions.data(); - if (VkDebugManager) + if (DebugManager) { VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; CreateInfo.enabledLayerCount = static_cast(ValidationLayers.size()); CreateInfo.ppEnabledLayerNames = ValidationLayers.data(); - VkDebugManager->PopulateDebugMessengerCreateInfo(debugCreateInfo); + DebugManager->PopulateDebugMessengerCreateInfo(debugCreateInfo); CreateInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*)&debugCreateInfo; } else diff --git a/src/private/VulkanPipeline.cpp b/src/private/VulkanPipeline.cpp new file mode 100755 index 0000000..a957711 --- /dev/null +++ b/src/private/VulkanPipeline.cpp @@ -0,0 +1,275 @@ +#include "VulkanPipeline.h" + +#include + +#include "utilities/FileReader.h" +#include "utilities/Logger.h" +#include "Primitives.h" + +void VulkanPipeline::Initialize(VkDevice InDevice) +{ + Device = InDevice; +} + +void VulkanPipeline::Cleanup() +{ + // for (auto Framebuffer : SwapChainFrameBuffers) + // { + // vkDestroyFramebuffer(Device, Framebuffer, nullptr); + // } + + vkDestroyPipeline(Device, GraphicsPipeline, nullptr); + vkDestroyPipelineLayout(Device, PipelineLayout, nullptr); +} + +void VulkanPipeline::CreateGraphicsPipeline(VkExtent2D SwapChainExtent, VkRenderPass RenderPass) +{ + auto VertShaderCode = ReadFile("Shaders/vert.spv"); + auto FragShaderCode = ReadFile("Shaders/frag.spv"); + + Log::Info("Vert buffer size: " + std::to_string(VertShaderCode.size())); + Log::Info("Frag buffer size: " + std::to_string(FragShaderCode.size())); + + VkShaderModule VertShaderModule = CreateShaderModule(VertShaderCode); + VkShaderModule FragShaderModule = CreateShaderModule(FragShaderCode); + + VkPipelineShaderStageCreateInfo VertShaderStageInfo{}; + VertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + VertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; + VertShaderStageInfo.module = VertShaderModule; + VertShaderStageInfo.pName = "main"; + + VkPipelineShaderStageCreateInfo FragShaderStageInfo{}; + FragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + FragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; + FragShaderStageInfo.module = FragShaderModule; + FragShaderStageInfo.pName = "main"; + + VkPipelineShaderStageCreateInfo ShaderStages[] = { VertShaderStageInfo, FragShaderStageInfo }; + + VkPipelineVertexInputStateCreateInfo VertexInputInfo{}; + VertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + auto BindingDescription = Vertex::GetBindingDescription(); + auto AttributeDescriptions = Vertex::GetAttributeDescriptions(); + + VertexInputInfo.vertexBindingDescriptionCount = 1; + VertexInputInfo.pVertexBindingDescriptions = &BindingDescription; + VertexInputInfo.vertexAttributeDescriptionCount = static_cast(AttributeDescriptions.size()); + VertexInputInfo.pVertexAttributeDescriptions = AttributeDescriptions.data(); + + VkPipelineInputAssemblyStateCreateInfo InputAssembly{}; + InputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + InputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + InputAssembly.primitiveRestartEnable = VK_FALSE; + + // VkViewport Viewport{}; + // Viewport.x = 0.0f; + // Viewport.x = 0.0f; + // Viewport.width = SwapChainExtent.width; + // Viewport.height = SwapChainExtent.height; + // Viewport.minDepth = 0.0f; + // Viewport.maxDepth = 1.0f; + + VkRect2D Scissor{}; + Scissor.offset = { 0, 0 }; + Scissor.extent = SwapChainExtent; + + std::vector DynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + VkPipelineDynamicStateCreateInfo DynamicState{}; + DynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + DynamicState.dynamicStateCount = static_cast(DynamicStates.size()); + DynamicState.pDynamicStates = DynamicStates.data(); + + VkPipelineViewportStateCreateInfo ViewportState{}; + ViewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + ViewportState.viewportCount = 1; + ViewportState.scissorCount = 1; + + VkPipelineRasterizationStateCreateInfo Rasterizer{}; + Rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + Rasterizer.depthClampEnable = VK_FALSE; + Rasterizer.rasterizerDiscardEnable = VK_FALSE; + Rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + Rasterizer.lineWidth = 1.0f; + Rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; + Rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; + Rasterizer.depthBiasEnable = VK_FALSE; + Rasterizer.depthBiasConstantFactor = 0.0f; + Rasterizer.depthBiasClamp = 0.0f; + Rasterizer.depthBiasSlopeFactor = 0.0f; + + VkPipelineMultisampleStateCreateInfo Multisampling{}; + Multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + Multisampling.sampleShadingEnable = VK_FALSE; + Multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + Multisampling.minSampleShading = 1.0f; + Multisampling.pSampleMask = nullptr; + Multisampling.alphaToCoverageEnable = VK_FALSE; + Multisampling.alphaToOneEnable = VK_FALSE; + + VkPipelineColorBlendAttachmentState ColorBlendAttachement{}; + ColorBlendAttachement.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + ColorBlendAttachement.blendEnable = VK_FALSE; + ColorBlendAttachement.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + ColorBlendAttachement.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; + ColorBlendAttachement.colorBlendOp = VK_BLEND_OP_ADD; + ColorBlendAttachement.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + ColorBlendAttachement.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + ColorBlendAttachement.alphaBlendOp = VK_BLEND_OP_ADD; + + VkPipelineColorBlendStateCreateInfo ColorBlending{}; + ColorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + ColorBlending.logicOpEnable = VK_FALSE; + ColorBlending.logicOp = VK_LOGIC_OP_COPY; + ColorBlending.attachmentCount = 1; + ColorBlending.pAttachments = &ColorBlendAttachement; + ColorBlending.blendConstants[0] = 0.0f; + ColorBlending.blendConstants[1] = 0.0f; + ColorBlending.blendConstants[2] = 0.0f; + ColorBlending.blendConstants[3] = 0.0f; + + VkPipelineLayoutCreateInfo PipelineLayoutInfo{}; + PipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + PipelineLayoutInfo.setLayoutCount = 0; + PipelineLayoutInfo.pSetLayouts = nullptr; + PipelineLayoutInfo.pushConstantRangeCount = 0; + PipelineLayoutInfo.pPushConstantRanges = nullptr; + + if (vkCreatePipelineLayout(Device, &PipelineLayoutInfo, nullptr, &PipelineLayout) != VK_SUCCESS) + { + Log::Error("Failed to create pipeline layout!"); + } + else + { + Log::Info("Successfully created pipeline layout"); + } + + VkGraphicsPipelineCreateInfo PipelineInfo{}; + PipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + PipelineInfo.stageCount = 2; + PipelineInfo.pStages = ShaderStages; + PipelineInfo.pVertexInputState = &VertexInputInfo; + PipelineInfo.pInputAssemblyState = &InputAssembly; + PipelineInfo.pViewportState = &ViewportState; + PipelineInfo.pRasterizationState = &Rasterizer; + PipelineInfo.pMultisampleState = &Multisampling; + PipelineInfo.pDepthStencilState = nullptr; + PipelineInfo.pColorBlendState = &ColorBlending; + PipelineInfo.pDynamicState = &DynamicState; + PipelineInfo.layout = PipelineLayout; + PipelineInfo.renderPass = RenderPass; + PipelineInfo.subpass = 0; + PipelineInfo.basePipelineHandle = VK_NULL_HANDLE; + PipelineInfo.basePipelineIndex = -1; + + if (vkCreateGraphicsPipelines(Device, VK_NULL_HANDLE, 1, &PipelineInfo, nullptr, &GraphicsPipeline) != VK_SUCCESS) + { + Log::Error("Failed to create graphics pipeline!"); + } + else + { + Log::Info("Successfully created graphics pipeline."); + } + + vkDestroyShaderModule(Device, FragShaderModule, nullptr); + vkDestroyShaderModule(Device, VertShaderModule, nullptr); +} + +VkShaderModule VulkanPipeline::CreateShaderModule(const std::vector& Code) +{ + VkShaderModuleCreateInfo CreateInfo{}; + CreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + CreateInfo.codeSize = Code.size(); + CreateInfo.pCode = reinterpret_cast(Code.data()); + + VkShaderModule ShaderModule; + if (vkCreateShaderModule(Device, &CreateInfo, nullptr, &ShaderModule) != VK_SUCCESS) + { + Log::Error("Failed to create shader module."); + } + else + { + Log::Info("Successfully created shader module."); + } + + return ShaderModule; +} + +// void VulkanPipeline::CreateRenderPass(VkFormat SwapChainImageFormat) +// { +// VkAttachmentDescription ColorAttachment{}; +// ColorAttachment.format = SwapChainImageFormat; +// ColorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; +// ColorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +// ColorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +// ColorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +// ColorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +// ColorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +// ColorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; +// +// VkAttachmentReference ColorAttachmentRef{}; +// ColorAttachmentRef.attachment = 0; +// ColorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +// +// VkSubpassDescription Subpass{}; +// Subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +// Subpass.colorAttachmentCount = 1; +// Subpass.pColorAttachments = &ColorAttachmentRef; +// +// VkRenderPassCreateInfo RenderPassInfo{}; +// RenderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; +// RenderPassInfo.attachmentCount = 1; +// RenderPassInfo.pAttachments = &ColorAttachment; +// RenderPassInfo.subpassCount = 1; +// RenderPassInfo.pSubpasses = &Subpass; +// +// VkSubpassDependency Dependency{}; +// Dependency.srcSubpass = VK_SUBPASS_EXTERNAL; +// Dependency.dstSubpass = 0; +// Dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +// Dependency.srcAccessMask = 0; +// Dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +// Dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; +// RenderPassInfo.dependencyCount = 1; +// RenderPassInfo.pDependencies = &Dependency; +// +// if (vkCreateRenderPass(Device, &RenderPassInfo, nullptr, &RenderPass) != VK_SUCCESS) +// { +// Log::Error("Failed to create render pass!"); +// } +// else +// { +// Log::Info("Successfully created render pass."); +// } +// } + +// void VulkanPipeline::CreateFramebuffers(std::vector SwapChainImageViews, VkExtent2D SwapChainExtent) +// { +// SwapChainFrameBuffers.resize(SwapChainImageViews.size()); +// +// for (size_t i = 0; i < SwapChainImageViews.size(); i++) +// { +// VkImageView Attachments[] = { +// SwapChainImageViews[i] +// }; +// +// VkFramebufferCreateInfo FramebufferInfo{}; +// FramebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; +// FramebufferInfo.renderPass = RenderPass; +// FramebufferInfo.attachmentCount = 1; +// FramebufferInfo.pAttachments = Attachments; +// FramebufferInfo.width = SwapChainExtent.width; +// FramebufferInfo.height = SwapChainExtent.height; +// FramebufferInfo.layers = 1; +// +// if (vkCreateFramebuffer(Device, &FramebufferInfo, nullptr, &SwapChainFrameBuffers[i]) != VK_SUCCESS) +// { +// Log::Error("Failed to create frame buffer!"); +// } +// } +// } diff --git a/src/private/VulkanRenderPass.cpp b/src/private/VulkanRenderPass.cpp new file mode 100644 index 0000000..34482d2 --- /dev/null +++ b/src/private/VulkanRenderPass.cpp @@ -0,0 +1,60 @@ +#include "VulkanRenderPass.h" +#include "utilities/Logger.h" + +void VulkanRenderPass::Initialize(VkDevice InDevice) +{ + Device = InDevice; +} + +void VulkanRenderPass::Cleanup() +{ + vkDestroyRenderPass(Device, RenderPass, nullptr); +} + +void VulkanRenderPass::CreateRenderPass(VkFormat SwapChainImageFormat) +{ + VkAttachmentDescription ColorAttachment{}; + ColorAttachment.format = SwapChainImageFormat; + ColorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; + ColorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + ColorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + ColorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + ColorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + ColorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + ColorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference ColorAttachmentRef{}; + ColorAttachmentRef.attachment = 0; + ColorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription Subpass{}; + Subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + Subpass.colorAttachmentCount = 1; + Subpass.pColorAttachments = &ColorAttachmentRef; + + VkRenderPassCreateInfo RenderPassInfo{}; + RenderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + RenderPassInfo.attachmentCount = 1; + RenderPassInfo.pAttachments = &ColorAttachment; + RenderPassInfo.subpassCount = 1; + RenderPassInfo.pSubpasses = &Subpass; + + VkSubpassDependency Dependency{}; + Dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + Dependency.dstSubpass = 0; + Dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + Dependency.srcAccessMask = 0; + Dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + Dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + RenderPassInfo.dependencyCount = 1; + RenderPassInfo.pDependencies = &Dependency; + + if (vkCreateRenderPass(Device, &RenderPassInfo, nullptr, &RenderPass) != VK_SUCCESS) + { + Log::Error("Failed to create render pass!"); + } + else + { + Log::Info("Successfully created render pass."); + } +} diff --git a/src/private/VulkanSwapChain.cpp b/src/private/VulkanSwapChain.cpp new file mode 100644 index 0000000..c0ef310 --- /dev/null +++ b/src/private/VulkanSwapChain.cpp @@ -0,0 +1,203 @@ +#include "VulkanSwapChain.h" + +#include +#include // Necessary for uint32_t +#include // Necessary for std::numeric_limits +#include // Necessary for std::clamp +#include +#include + +#include "utilities/Logger.h" + +VulkanSwapChain::VulkanSwapChain() +{ +} + +VulkanSwapChain::~VulkanSwapChain() +{ + // Cleanup(); +} + +void VulkanSwapChain::Initialize(FSwapConfig InSwapConfig) +{ + SwapConfig = InSwapConfig; +} + +// void VulkanSwapChain::Initialize( +// VkDevice InDevice, +// VkSurfaceKHR InSurface, +// GLFWwindow* InWindow) +// { +// Device = InDevice; +// Surface = InSurface; +// Window = InWindow; +// } + +void VulkanSwapChain::Cleanup() +{ + for (auto ImageView : SwapChainImageViews) + { + vkDestroyImageView(SwapConfig.Device, ImageView, nullptr); + } + vkDestroySwapchainKHR(SwapConfig.Device, SwapChain, nullptr); +} + +VkSurfaceFormatKHR VulkanSwapChain::ChooseSwapSurfaceFormat(const std::vector& AvailableFormats) +{ + for (const auto& AvailableFormat : AvailableFormats) + { + if (AvailableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && AvailableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + return AvailableFormat; + } + } + return AvailableFormats[0]; +} + +VkPresentModeKHR VulkanSwapChain::ChooseSwapPresentMode(const std::vector& AvailablePresentModes) +{ + for (const auto& AvailablePresentMode : AvailablePresentModes) + { + if (AvailablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) + { + return AvailablePresentMode; + } + } + return VK_PRESENT_MODE_FIFO_KHR; +} + +VkExtent2D VulkanSwapChain::ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& Capabilities) +{ + if (Capabilities.currentExtent.width != (std::numeric_limits::max)()) + { + return Capabilities.currentExtent; + } + else + { + if (SwapConfig.Window == nullptr) + { + Log::Error("GLFW window is null in CreateSwapChain!"); + } + int Width, Height; + glfwGetFramebufferSize(SwapConfig.Window, &Width, &Height); + + VkExtent2D ActualExtent = { + static_cast(Width), + static_cast(Height) + }; + + ActualExtent.width = std::clamp(ActualExtent.width, Capabilities.minImageExtent.width, Capabilities.maxImageExtent.width); + ActualExtent.height = std::clamp(ActualExtent.height, Capabilities.minImageExtent.height, Capabilities.maxImageExtent.height); + + return ActualExtent; + } +} + +void VulkanSwapChain::CreateSwapChain() +{ + VkSurfaceFormatKHR SurfaceFormat = ChooseSwapSurfaceFormat(SwapConfig.Formats); + VkPresentModeKHR PresentMode = ChooseSwapPresentMode(SwapConfig.PresentModes); + VkExtent2D Extent = ChooseSwapExtent(SwapConfig.Capabilities); + + uint32_t ImageCount = SwapConfig.Capabilities.minImageCount + 1; + + if (SwapConfig.Capabilities.maxImageCount > 0 && ImageCount > SwapConfig.Capabilities.maxImageCount) + { + ImageCount = SwapConfig.Capabilities.maxImageCount; + } + + VkSwapchainCreateInfoKHR CreateInfo{}; + CreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + CreateInfo.surface = SwapConfig.Surface; + + CreateInfo.minImageCount = ImageCount; + CreateInfo.imageFormat = SurfaceFormat.format; + CreateInfo.imageColorSpace = SurfaceFormat.colorSpace; + CreateInfo.imageExtent = Extent; + CreateInfo.imageArrayLayers = 1; + CreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; // may need VK_IMAGE_USAGE_TRANSFER_DST_BIT for post processing https://vulkan-tutorial.com/Drawing_a_triangle/Presentation/Swap_chain#:~:text=VK%5FIMAGE%5FUSAGE%5FTRANSFER%5FDST%5FBIT + + uint32_t QueueFamilyIndices[] = { SwapConfig.GraphicsFamily.value(), + SwapConfig.PresentFamily.value() }; + + if (SwapConfig.GraphicsFamily != SwapConfig.PresentFamily) + { + CreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + CreateInfo.queueFamilyIndexCount = 2; + CreateInfo.pQueueFamilyIndices = QueueFamilyIndices; + } + else + { + CreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + CreateInfo.queueFamilyIndexCount = 0; + CreateInfo.pQueueFamilyIndices = nullptr; + } + + CreateInfo.preTransform = SwapConfig.Capabilities.currentTransform; + CreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + CreateInfo.presentMode = PresentMode; + CreateInfo.clipped = VK_TRUE; + CreateInfo.oldSwapchain = VK_NULL_HANDLE; + + Log::Info("ChooseSwapExtent 3"); + if (vkCreateSwapchainKHR(SwapConfig.Device, &CreateInfo, nullptr, &SwapChain) != VK_SUCCESS) + { + Log::Error("Failed to create swap chain."); + } + else + { + Log::Info("Successfully created swap chain."); + } + + vkGetSwapchainImagesKHR(SwapConfig.Device, SwapChain, &ImageCount, nullptr); + SwapChainImages.resize(ImageCount); + vkGetSwapchainImagesKHR(SwapConfig.Device, SwapChain, &ImageCount, SwapChainImages.data()); + + SwapChainImageFormat = SurfaceFormat.format; + SwapChainExtent = Extent; +} + +void VulkanSwapChain::CreateImageViews() +{ + SwapChainImageViews.resize(SwapChainImages.size()); + + int CreatedViews = 0; + for (size_t i = 0; i < SwapChainImages.size(); i++) + { + VkImageViewCreateInfo CreateInfo{}; + CreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + CreateInfo.image = SwapChainImages[i]; + + CreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + CreateInfo.format = SwapChainImageFormat; + CreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + CreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + + CreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + CreateInfo.subresourceRange.baseMipLevel = 0; + CreateInfo.subresourceRange.levelCount = 1; + CreateInfo.subresourceRange.baseArrayLayer = 0; + CreateInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(SwapConfig.Device, &CreateInfo, nullptr, &SwapChainImageViews[i]) != VK_SUCCESS) + { + Log::Error("Failed to create image views."); + } + else + { + CreatedViews++; + } + } + + Log::Info("Successfully created " + std::to_string(CreatedViews) + " image views."); +} + +// void VulkanSwapChain::RecreateSwapChain() +// { +// vkDeviceWaitIdle(SwapConfig.Device); +// +// CreateSwapChain(); +// CreateImageViews(); +// } diff --git a/src/private/VulkanVertexBuffer.cpp b/src/private/VulkanVertexBuffer.cpp new file mode 100644 index 0000000..96e37b4 --- /dev/null +++ b/src/private/VulkanVertexBuffer.cpp @@ -0,0 +1,74 @@ +#include "VulkanVertexBuffer.h" +#include "utilities/Logger.h" +#include +#include +#include + +void VulkanVertexBuffer::Initialize(FVertexBufferConfig InConfig) +{ + Config = InConfig; +} + +void VulkanVertexBuffer::Cleanup() +{ + vkDestroyBuffer(Config.Device, VertexBuffer, nullptr); + vkFreeMemory(Config.Device, VertexBufferMemory, nullptr); +} + +void VulkanVertexBuffer::CreateVertexBuffer(std::vector InVertices) +{ + VkBufferCreateInfo BufferInfo{}; + BufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + BufferInfo.size = sizeof(InVertices[0]) * InVertices.size(); + BufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + BufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(Config.Device, &BufferInfo, nullptr, &VertexBuffer) != VK_SUCCESS) + { + Log::Error("Failed to create vertex buffer!"); + } + else + { + Log::Info("Successfully created vertex buffer."); + } + + VkMemoryRequirements MemoryRequirements; + vkGetBufferMemoryRequirements(Config.Device, VertexBuffer, &MemoryRequirements); + + VkMemoryAllocateInfo AllocateInfo{}; + AllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + AllocateInfo.allocationSize = MemoryRequirements.size; + AllocateInfo.memoryTypeIndex = FindMemoryType(MemoryRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + if (vkAllocateMemory(Config.Device, &AllocateInfo, nullptr, &VertexBufferMemory) != VK_SUCCESS) + { + Log::Error("Failed to allocate vertex buffer memory!"); + } + else + { + Log::Info("Successfully allocated vertex buffer memory."); + } + + vkBindBufferMemory(Config.Device, VertexBuffer, VertexBufferMemory, 0); + + void* Data; + vkMapMemory(Config.Device, VertexBufferMemory, 0, BufferInfo.size, 0, &Data); + memcpy(Data, InVertices.data(), (size_t)BufferInfo.size); + vkUnmapMemory(Config.Device, VertexBufferMemory); +} + +uint32_t VulkanVertexBuffer::FindMemoryType(uint32_t TypeFilter, VkMemoryPropertyFlags Properties) +{ + VkPhysicalDeviceMemoryProperties MemoryProperties; + vkGetPhysicalDeviceMemoryProperties(Config.PhysicalDevice, &MemoryProperties); + + for (uint32_t i = 0; i < MemoryProperties.memoryTypeCount; i++) + { + if ((TypeFilter & (1 << i)) && (MemoryProperties.memoryTypes[i].propertyFlags & Properties) == Properties) + { + return i; + } + } + + Log::Error("Failed to find suitable memory type!"); +} diff --git a/src/GlfwWindowManager.h b/src/public/GlfwWindowManager.h old mode 100644 new mode 100755 similarity index 66% rename from src/GlfwWindowManager.h rename to src/public/GlfwWindowManager.h index 3361f49..362315f --- a/src/GlfwWindowManager.h +++ b/src/public/GlfwWindowManager.h @@ -2,11 +2,11 @@ #include -#define GLFW_INCLUDE_VULKAN +#include +#define GLFW_INCLUDE_VULKAN #include #include -#include struct FWindowConfig { @@ -23,26 +23,19 @@ public: GlfwWindowManager(); ~GlfwWindowManager(); - // GlfwWindowManager(const GlfwWindowManager&) = delete; - // GlfwWindowManager& operator=(const GlfwWindowManager&) = delete; - - // GlfwWindowManager(GlfwWindowManager&& Other) noexcept; - // GlfwWindowManager& operator=(GlfwWindowManager&& Other) noexcept; - void Initialize(const FWindowConfig& Config); - void Cleanup(VkInstance Instance); + void Cleanup(); bool ShouldClose() const; void PollEvents(); void WaitEvents() const; void SetTitle(const std::string& Title); - GLFWwindow* GetHandle() const { return Window; } uint32_t GetWidth() const { return Config.Width; } uint32_t GetHeight() const { return Config.Height; } const std::string& GetTitle() const { return Config.Title; } - bool IsInitialized() const { return Window && Surface; } + bool IsInitialized() const { return Window; } void SetResizeCallback(GLFWwindowsizefun Callback); void SetKeyCallback(GLFWkeyfun Callback); @@ -50,16 +43,11 @@ public: void SetCursorPositionCallback(GLFWcursorposfun Callback); void SetScrollCallback(GLFWscrollfun Callback); - void CreateSurface(VkInstance Instance); - - static VkSurfaceKHR Surface; - - static GLFWwindow* Window; + GLFWwindow* GetWindow() const { return Window; } private: FWindowConfig Config; - - // VkSurfaceKHR Surface = VK_NULL_HANDLE; + GLFWwindow* Window; void InitializeGlfw(); diff --git a/src/public/Primitives.h b/src/public/Primitives.h new file mode 100644 index 0000000..7398001 --- /dev/null +++ b/src/public/Primitives.h @@ -0,0 +1,36 @@ +#include +#include + +#include + +struct Vertex +{ + glm::vec2 Position; + glm::vec3 Color; + + static VkVertexInputBindingDescription GetBindingDescription() + { + VkVertexInputBindingDescription BindingDescription{}; + BindingDescription.binding = 0; + BindingDescription.stride = sizeof(Vertex); + BindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + return BindingDescription; + } + + static std::array GetAttributeDescriptions() + { + std::array AttributeDescriptions{}; + AttributeDescriptions[0].binding = 0; + AttributeDescriptions[0].location = 0; + AttributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + AttributeDescriptions[0].offset = offsetof(Vertex, Position); + + AttributeDescriptions[1].binding = 0; + AttributeDescriptions[1].location = 1; + AttributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + AttributeDescriptions[1].offset = offsetof(Vertex, Color); + + return AttributeDescriptions; + } +}; diff --git a/src/public/VulkanCommandBuffers.h b/src/public/VulkanCommandBuffers.h new file mode 100644 index 0000000..1d6d84e --- /dev/null +++ b/src/public/VulkanCommandBuffers.h @@ -0,0 +1,45 @@ + +#pragma once + +#include "VulkanVertexBuffer.h" +#include +#define GLFW_INCLUDE_VULKAN +#include + +#include +#include + +class VulkanCommandBuffers +{ +public: + void Initialize( + VkDevice InDevice, + VkRenderPass InRenderPass); + + void Cleanup(); + + void CreateCommandPool(std::optional GraphicsFamily); + + void CreateCommandBuffers(int FramesInFlight); + + void RecordCommandBuffer( + VkCommandBuffer InCommandBuffer, + uint32_t ImageIndex, + VkBuffer InVertexBuffer, + std::vector InVertices, + VkRenderPass RenderPass, + VkExtent2D SwapChainExtent, + VkPipeline GraphicsPipeline, + std::vector SwapChainFramebuffers); + + std::vector GetCommandBuffers() { return CommandBuffers; } + VkCommandBuffer GetCommandBuffer(int i) { return CommandBuffers[i]; } + +private: + VkDevice Device; + VkRenderPass RenderPass; + VkQueue GraphicsQueue; + + std::vector CommandBuffers; + VkCommandPool CommandPool; +}; diff --git a/src/public/VulkanContext.h b/src/public/VulkanContext.h new file mode 100755 index 0000000..e91cb79 --- /dev/null +++ b/src/public/VulkanContext.h @@ -0,0 +1,77 @@ +#pragma once + +#include "VulkanCommandBuffers.h" +#include "VulkanFramebuffers.h" +#include "VulkanInstanceManager.h" +#include "VulkanDeviceManager.h" +#include "VulkanDebugManager.h" +#include "VulkanPipeline.h" +#include "VulkanRenderPass.h" +#include "VulkanSwapChain.h" +#include "VulkanVertexBuffer.h" + +#include +#include + +#define GLFW_INCLUDE_VULKAN +#include + +static const std::vector ValidationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +struct FVulkanConfig +{ + bool bValidationEnabled = true; + bool bVerboseLogging = false; + GLFWwindow* Window = nullptr; + std::vector Vertices; +}; + +static FVulkanConfig Config = {}; + +class VulkanContext +{ +public: + VulkanContext(); + ~VulkanContext(); + + void Initialize(FVulkanConfig& InConfig); + void Cleanup(); + + void CreateSurface(GLFWwindow* Window); + void CreateSyncObjects(); + void DrawFrame(); + void RecreateSwapChain(); + void CleanupSwapChain(); + + VkSurfaceKHR GetSurface() { return Surface; }; + + void SetFramebufferResized(bool bResized) { bFramebufferResized = bResized; } + +private: + VkSurfaceKHR Surface = VK_NULL_HANDLE; + + VulkanInstanceManager InstanceManager; + VulkanDebugManager DebugManager; + VulkanDeviceManager DeviceManager; + VulkanSwapChain SwapChain; + VulkanPipeline GraphicsPipeline; + VulkanRenderPass RenderPass; + VulkanFramebuffers Framebuffers; + VulkanVertexBuffer VertexBuffer; + VulkanCommandBuffers CommandBuffers; + + // VkDevice Device; + // VkPhysicalDevice PhysicalDevice; + // VkQueue GraphicsQueue; + + std::vector ImageAvailableSemaphores; + std::vector RenderFinishedSemaphores; + std::vector InFlightFences; + + bool bFramebufferResized = false; + + const int MAX_FRAMES_IN_FLIGHT = 2; + uint32_t CurrentFrame = 0; +}; diff --git a/src/VulkanDebugManager.h b/src/public/VulkanDebugManager.h old mode 100644 new mode 100755 similarity index 67% rename from src/VulkanDebugManager.h rename to src/public/VulkanDebugManager.h index fa50127..60b3991 --- a/src/VulkanDebugManager.h +++ b/src/public/VulkanDebugManager.h @@ -3,7 +3,7 @@ #define GLFW_INCLUDE_VULKAN #include -struct FCreateDebugUtilsMessengerExtParams +struct FCreateParams { VkInstance Instance; const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo; @@ -11,7 +11,7 @@ struct FCreateDebugUtilsMessengerExtParams VkDebugUtilsMessengerEXT* pDebugMessenger; }; -struct FDestroyDebugUtilsMessengerExtParams +struct FDestroyParams { VkInstance Instance; VkDebugUtilsMessengerEXT DebugMessenger; @@ -24,12 +24,6 @@ public: VulkanDebugManager(); ~VulkanDebugManager(); - VulkanDebugManager(const VulkanDebugManager&) = delete; - VulkanDebugManager& operator=(const VulkanDebugManager&) = delete; - - VulkanDebugManager(VulkanDebugManager&& Other) noexcept; - VulkanDebugManager& operator=(VulkanDebugManager&& Other) noexcept; - void Initialize(VkInstance Instance); void Cleanup(); @@ -44,9 +38,9 @@ private: VkDebugUtilsMessengerEXT DebugMessenger = VK_NULL_HANDLE; - VkResult CreateDebugUtilsMessengerExt(const FCreateDebugUtilsMessengerExtParams& Params); + VkResult CreateDebugUtilsMessengerExt(const FCreateParams& Params); - void DestroyDebugUtilsMessengerExt(const FDestroyDebugUtilsMessengerExtParams& Params); + void DestroyDebugUtilsMessengerExt(const FDestroyParams& Params); static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback( VkDebugUtilsMessageSeverityFlagBitsEXT MessageSeverity, diff --git a/src/public/VulkanDeviceManager.h b/src/public/VulkanDeviceManager.h new file mode 100755 index 0000000..49d15e7 --- /dev/null +++ b/src/public/VulkanDeviceManager.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +#define GLFW_INCLUDE_VULKAN +#include + +struct FDeviceConfig +{ + VkInstance Instance; + bool bEnableValidationLayers; + VkSurfaceKHR Surface; + GLFWwindow* Window; +}; + +struct QueueFamilyIndices +{ + std::optional GraphicsFamily; + std::optional PresentFamily; + + bool IsComplete() + { + return GraphicsFamily.has_value() && PresentFamily.has_value(); + } +}; + +struct SwapChainSupportDetails +{ + VkSurfaceCapabilitiesKHR Capabilities; + std::vector Formats; + std::vector PresentModes; +}; + +class VulkanDeviceManager +{ +public: + VulkanDeviceManager(); + ~VulkanDeviceManager(); + + void Initialize(FDeviceConfig InConfig); + // VkInstance Instance, + // bool EnableValidationLayers, + // VkSurfaceKHR Surface, + // GLFWwindow* Window); + + void Cleanup(); + + void PickPhysicalDevice(); + void CreateLogicalDevice(); + + VkDevice GetDevice() { return Device; } + VkPhysicalDevice GetPhysicalDevice() { return PhysicalDevice; } + QueueFamilyIndices GetPhysicalQueueFamilies() { return PhysicalQueueFamilies; } + VkQueue GetGraphicsQueue() { return GraphicsQueue; } + VkQueue GetPresentQueue() { return PresentQueue; } + SwapChainSupportDetails GetSwapChainSupport() { return SwapChainSupport; } + +private: + FDeviceConfig DeviceConfig; + + // VkInstance Instance = VK_NULL_HANDLE; + VkDevice Device = VK_NULL_HANDLE; + VkPhysicalDevice PhysicalDevice = VK_NULL_HANDLE; + VkQueue GraphicsQueue = VK_NULL_HANDLE; + VkQueue PresentQueue = VK_NULL_HANDLE; + QueueFamilyIndices PhysicalQueueFamilies; + // VkSurfaceKHR Surface; + + // GLFWwindow* Window = nullptr; + + SwapChainSupportDetails SwapChainSupport; + + // bool bEnableValidationLayers = false; + + bool IsDeviceSuitable(VkPhysicalDevice Device); + + int RateDeviceSuitability(VkPhysicalDevice Device); + + bool CheckDeviceExtensionSupport(VkPhysicalDevice Device); + + QueueFamilyIndices FindQueueFamilies(VkPhysicalDevice Device); + + SwapChainSupportDetails QuerySwapChainSupport(VkPhysicalDevice Device); +}; diff --git a/src/public/VulkanFramebuffers.h b/src/public/VulkanFramebuffers.h new file mode 100644 index 0000000..b9a6045 --- /dev/null +++ b/src/public/VulkanFramebuffers.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#define GLFW_INCLUDE_VULKAN +#include + +#include + +struct FFramebufferConfig +{ + VkDevice Device; + VkRenderPass RenderPass; + std::vector SwapChainImageViews; + VkExtent2D SwapChainExtent; +}; + +class VulkanFramebuffers +{ +public: + void Initialize(FFramebufferConfig InConfig); + + void Cleanup(); + + void CreateFramebuffers(); + // void CreateFramebuffers(VkRenderPass RenderPass, std::vector SwapChainImageViews, VkExtent2D SwapChainExtent); + + std::vector GetSwapChainFrameBuffers() { return SwapChainFramebuffers; } + + // void CreateCommandPool(std::optional GraphicsFamily); + // + // void CreateCommandBuffer(); + // + // void RecordCommandBuffer(VkCommandBuffer CommandBuffer, uint32_t imageIndex, VkRenderPass RenderPass, VkExtent2D SwapChainExtent, VkPipeline GraphicsPipeline); + +private: + FFramebufferConfig FramebufferConfig; + + std::vector SwapChainFramebuffers; +}; diff --git a/src/public/VulkanInstanceManager.h b/src/public/VulkanInstanceManager.h new file mode 100755 index 0000000..18fbf12 --- /dev/null +++ b/src/public/VulkanInstanceManager.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#define GLFW_INCLUDE_VULKAN +#include + +#include "VulkanDebugManager.h" + +class VulkanInstanceManager +{ +public: + VulkanInstanceManager(); + ~VulkanInstanceManager(); + + void Cleanup(); + + void CreateInstance(VulkanDebugManager* DebugManager = nullptr); + const VkInstance GetInstance() const { return Instance; } + +private: + VkInstance Instance = VK_NULL_HANDLE; + + std::vector GetRequiredExtensions(bool bEnableValidationLayers); + + bool CheckValidationLayerSupport(); +}; diff --git a/src/public/VulkanPipeline.h b/src/public/VulkanPipeline.h new file mode 100755 index 0000000..8cf5067 --- /dev/null +++ b/src/public/VulkanPipeline.h @@ -0,0 +1,33 @@ +#pragma once + +#define GLFW_INCLUDE_VULKAN +#include + +#include + +class VulkanPipeline +{ +public: + void Initialize(VkDevice InDevice); + + void Cleanup(); + + // void CreateRenderPass(VkFormat SwapChainImageFormat); + + // void CreateFramebuffers(std::vector SwapChainImageViews, VkExtent2D SwapChainExtent); + + VkShaderModule CreateShaderModule(const std::vector& Code); + + void CreateGraphicsPipeline(VkExtent2D SwapChainExtent, VkRenderPass RenderPass); + // + // VkRenderPass GetRenderPass() { return RenderPass; } + // + VkPipeline GetGraphicsPipeline() { return GraphicsPipeline; } + +private: + VkDevice Device; + // VkRenderPass RenderPass; + VkPipelineLayout PipelineLayout; + VkPipeline GraphicsPipeline; + // std::vector SwapChainFrameBuffers; +}; diff --git a/src/public/VulkanRenderPass.h b/src/public/VulkanRenderPass.h new file mode 100644 index 0000000..9aa9a87 --- /dev/null +++ b/src/public/VulkanRenderPass.h @@ -0,0 +1,20 @@ +#pragma once + +#define GLFW_INCLUDE_VULKAN +#include + +class VulkanRenderPass +{ +public: + void Initialize(VkDevice InDevice); + + void Cleanup(); + + void CreateRenderPass(VkFormat SwapChainImageFormat); + + VkRenderPass GetRenderPass() { return RenderPass; } + +private: + VkDevice Device; + VkRenderPass RenderPass; +}; diff --git a/src/public/VulkanSwapChain.h b/src/public/VulkanSwapChain.h new file mode 100644 index 0000000..5182e74 --- /dev/null +++ b/src/public/VulkanSwapChain.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +#define GLFW_INCLUDE_VULKAN +#include + +const std::vector DeviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME +}; +// +// struct SwapChainSupportDetails +// { +// VkSurfaceCapabilitiesKHR Capabilities; +// std::vector Formats; +// std::vector PresentModes; +// }; + +struct FSwapConfig +{ + VkDevice Device; + VkSurfaceKHR Surface; + GLFWwindow* Window; + std::optional GraphicsFamily; + std::optional PresentFamily; + VkSurfaceCapabilitiesKHR Capabilities; + std::vector Formats; + std::vector PresentModes; +}; + +class VulkanSwapChain +{ +public: + VulkanSwapChain(); + ~VulkanSwapChain(); + + void Initialize(FSwapConfig InConfig); + + // void Initialize( + // VkDevice InDevice, + // VkSurfaceKHR InSurface, + // GLFWwindow* InWindow); + + void Cleanup(); + + void CreateSwapChain(); + // void CreateSwapChain( + // std::optional GraphicsFamily, + // std::optional PresentFamily, + // VkSurfaceCapabilitiesKHR Capabilities, + // std::vector Formats, + // std::vector PresentModes); + + void CreateImageViews(); + + // void RecreateSwapChain(); + + VkSwapchainKHR GetSwapChain() { return SwapChain; } + VkFormat GetSwapChainImageFormat() { return SwapChainImageFormat; } + VkExtent2D GetSwapChainExtent() { return SwapChainExtent; } + std::vector GetSwapChainImageViews() { return SwapChainImageViews; } + +private: + FSwapConfig SwapConfig; + // VkPhysicalDevice PhysicalDevice; + // VkDevice Device; + // VkSurfaceKHR Surface; + // + // GLFWwindow* Window = nullptr; + + std::vector SwapChainImages; + VkSwapchainKHR SwapChain = VK_NULL_HANDLE; + VkFormat SwapChainImageFormat; + VkExtent2D SwapChainExtent; + + std::vector SwapChainImageViews; + + // bool IsDeviceSuitable(VkPhysicalDevice Device); + // + // int RateDeviceSuitability(VkPhysicalDevice Device); + // + // bool CheckDeviceExtensionSupport(VkPhysicalDevice Device); + + // SwapChainSupportDetails QuerySwapChainSupport(VkPhysicalDevice Device); + + VkSurfaceFormatKHR ChooseSwapSurfaceFormat(const std::vector& AvailableFormats); + + VkPresentModeKHR ChooseSwapPresentMode(const std::vector& AvailablePresentModes); + + VkExtent2D ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& Capabilities); +}; diff --git a/src/public/VulkanVertexBuffer.h b/src/public/VulkanVertexBuffer.h new file mode 100644 index 0000000..f4a7daa --- /dev/null +++ b/src/public/VulkanVertexBuffer.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Primitives.h" + +#include +#include +#include + +#define GLFW_INCLUDE_VULKAN +#include + +struct FVertexBufferConfig +{ + VkDevice Device; + VkPhysicalDevice PhysicalDevice; +}; + +class VulkanVertexBuffer +{ +public: + void Initialize(FVertexBufferConfig InConfig); + + void Cleanup(); + + void CreateVertexBuffer(std::vector InVertices); + + uint32_t FindMemoryType(uint32_t TypeFilter, VkMemoryPropertyFlags Properties); + + VkBuffer GetVertexBuffer() { return VertexBuffer; } + +private: + FVertexBufferConfig Config; + VkBuffer VertexBuffer; + VkDeviceMemory VertexBufferMemory; +}; diff --git a/include/FileReader.h b/src/utilities/FileReader.h old mode 100644 new mode 100755 similarity index 88% rename from include/FileReader.h rename to src/utilities/FileReader.h index 6dd476e..463947b --- a/include/FileReader.h +++ b/src/utilities/FileReader.h @@ -1,26 +1,26 @@ -#pragma once - -#include -#include - -#include "Logger.h" - -static std::vector ReadFile(const std::string& FileName) -{ - std::ifstream File(FileName, std::ios::ate | std::ios::binary); - - if (!File.is_open()) - { - Log::Error("Failed to open file: " + FileName); - } - - size_t FileSize = (size_t)File.tellg(); - std::vector Buffer(FileSize); - - File.seekg(0); - File.read(Buffer.data(), FileSize); - - File.close(); - - return Buffer; -} \ No newline at end of file +#pragma once + +#include +#include + +#include "utilities/Logger.h" + +static std::vector ReadFile(const std::string& FileName) +{ + std::ifstream File(FileName, std::ios::ate | std::ios::binary); + + if (!File.is_open()) + { + Log::Error("Failed to open file: " + FileName); + } + + size_t FileSize = (size_t)File.tellg(); + std::vector Buffer(FileSize); + + File.seekg(0); + File.read(Buffer.data(), FileSize); + + File.close(); + + return Buffer; +} diff --git a/include/Logger.cpp b/src/utilities/Logger.cpp old mode 100644 new mode 100755 similarity index 98% rename from include/Logger.cpp rename to src/utilities/Logger.cpp index 0b8f110..16495b9 --- a/include/Logger.cpp +++ b/src/utilities/Logger.cpp @@ -1,4 +1,4 @@ -#include "Logger.h" +#include "utilities/Logger.h" #include void Log::Message(Level Level, const std::string& Message, const std::source_location& Location) @@ -61,4 +61,4 @@ void Log::Message(Level Level, const std::string& Message, const std::source_loc { throw std::runtime_error(LogMessage); } -} \ No newline at end of file +} diff --git a/include/Logger.h b/src/utilities/Logger.h old mode 100644 new mode 100755 similarity index 100% rename from include/Logger.h rename to src/utilities/Logger.h