Project
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
#include "app.h"
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
void Application::run() {
|
||||
initWindow();
|
||||
initVulkan();
|
||||
mainLoop();
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void Application::initVulkan() {
|
||||
createInstance();
|
||||
setupDebugMessenger();
|
||||
createSurface();
|
||||
pickPhysicalDevice();
|
||||
createLogicalDevice();
|
||||
createSwapChain();
|
||||
createImageViews();
|
||||
createGraphicsPipeline();
|
||||
createCommandPool();
|
||||
createCommandBuffer();
|
||||
createSyncObjects();
|
||||
}
|
||||
|
||||
void Application::mainLoop() {
|
||||
// Loop until the window should be closed
|
||||
while (!glfwWindowShouldClose(m_window)) {
|
||||
// Poll for new window events
|
||||
glfwPollEvents();
|
||||
// Draw the next frame
|
||||
drawFrame();
|
||||
}
|
||||
|
||||
// Once the window is closed, its time to clean up everything. Which once we are out of this function GPU objects
|
||||
// are de-allocated Since there is a high chance the GPU is still working on stuff, we need to wait for the device
|
||||
// to idle before we return.
|
||||
m_device.waitIdle();
|
||||
}
|
||||
|
||||
void Application::drawFrame() {
|
||||
// Wait for the previous frame to end
|
||||
auto fenceResult = m_device.waitForFences(*m_inFlightFences[m_frameIndex], vk::True, UINT64_MAX);
|
||||
// Get the next image in the swapchain
|
||||
auto [result, imageIndex] =
|
||||
m_swapchain.acquireNextImage(UINT64_MAX, *m_presentCompleteSemaphores[m_frameIndex], nullptr);
|
||||
|
||||
// Check to make sure the swapchain didn't report an eErrorOutOfDateKHR error
|
||||
if (result == vk::Result::eErrorOutOfDateKHR) {
|
||||
// Recreate the swapchain
|
||||
recreateSwapchain();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the result isn't a eSuccess or eSuboptimalKHR, something has gone wrong and we should report on it.
|
||||
if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) {
|
||||
throw std::runtime_error("Failed to acquire next swapchain image");
|
||||
}
|
||||
|
||||
// Reset the fence of the current image
|
||||
m_device.resetFences(*m_inFlightFences[m_frameIndex]);
|
||||
|
||||
// Record the command buffer
|
||||
recordCommandBuffer(imageIndex);
|
||||
|
||||
// Submit the command buffer to the queue
|
||||
vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput);
|
||||
const vk::SubmitInfo submitInfo{// Wait for the current frames present semaphore
|
||||
.waitSemaphoreCount = 1,
|
||||
.pWaitSemaphores = &*m_presentCompleteSemaphores[m_frameIndex],
|
||||
// Set our destination
|
||||
.pWaitDstStageMask = &waitDestinationStageMask,
|
||||
// Pass the command buffer/s to submit
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &*m_commandBuffers[m_frameIndex],
|
||||
// Pass the rendering semaphore to signal when done
|
||||
.signalSemaphoreCount = 1,
|
||||
.pSignalSemaphores = &*m_renderFinishedSemaphores[imageIndex]};
|
||||
|
||||
// Submit to the graphics queue
|
||||
m_queue.submit(submitInfo, *m_inFlightFences[m_frameIndex]);
|
||||
|
||||
|
||||
// Present the image
|
||||
try {
|
||||
const vk::PresentInfoKHR presentInfo{
|
||||
// Wait for the rendering semaphore
|
||||
.waitSemaphoreCount = 1,
|
||||
.pWaitSemaphores = &*m_renderFinishedSemaphores[imageIndex],
|
||||
// Pass the swapchain we want to present with
|
||||
.swapchainCount = 1,
|
||||
.pSwapchains = &*m_swapchain,
|
||||
// Pass the imageIndex of image we want to present in the swapchain
|
||||
.pImageIndices = &imageIndex,
|
||||
.pResults = nullptr // Optional, allows you to specify an array of vk::Result values to check for every
|
||||
// swapchain. We only have one so we can just use the vk::Result returned from the
|
||||
// present function
|
||||
};
|
||||
result = m_queue.presentKHR(presentInfo);
|
||||
|
||||
// If the result is eErrorOutOfDateKHR || eSuboptimalKHR || if the windows framebuffer has been resized we need
|
||||
// to recreate the swapchain
|
||||
if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR || m_framebufferResized) {
|
||||
m_framebufferResized = false;
|
||||
recreateSwapchain();
|
||||
} else if (result != vk::Result::eSuccess) {
|
||||
// If we get any other error, we should error out
|
||||
throw std::runtime_error("Failed to present swapchain image");
|
||||
}
|
||||
} catch (const vk::SystemError &e) {
|
||||
// There is a chance that a SystemError will be thrown when presenting. The SystemError.code() could also be a
|
||||
// eErrorOutOfDateKHR as well, so that should also be checked for so we can just recreate the swapchain as
|
||||
// normal. All other errors should crash out.
|
||||
if (e.code().value() == static_cast<int>(vk::Result::eErrorOutOfDateKHR)) {
|
||||
recreateSwapchain();
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate the frame index
|
||||
m_frameIndex = (m_frameIndex + 1) % MAX_FRAMES_IN_FLIGHT;
|
||||
}
|
||||
|
||||
void Application::cleanup() {
|
||||
// Destroy the swapchain
|
||||
cleanupSwapchain();
|
||||
|
||||
// Destroy the window
|
||||
glfwDestroyWindow(m_window);
|
||||
// Terminate GLFW
|
||||
glfwTerminate();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
// INCLUDES //
|
||||
|
||||
// Include vulkan
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "vulkan/vulkan.hpp"
|
||||
#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES)
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#else
|
||||
import vulkan_hpp;
|
||||
#endif
|
||||
|
||||
#define GLFW_INCLUDE_VULKAN
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
#define VK_USE_PLATFORM_WIN32_KHR
|
||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||
#elif defined(PLATFORM_LINUX)
|
||||
#define GLFW_EXPOSE_NATIVE_WAYLAND
|
||||
#elif defined(PLATFORM_MACOS)
|
||||
#define VK_USE_PLATFORM_MACOS_MVK
|
||||
#defined GLFW_EXPOSE_NATIVE_COCOA
|
||||
#endif
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <GLFW/glfw3native.h>
|
||||
|
||||
// The window width and height
|
||||
constexpr uint32_t WIDTH = 800;
|
||||
constexpr uint32_t HEIGHT = 600;
|
||||
constexpr int MAX_FRAMES_IN_FLIGHT = 2;
|
||||
|
||||
const std::vector<char const *> validationLayers = {"VK_LAYER_KHRONOS_validation"};
|
||||
|
||||
#ifdef NDEBUG
|
||||
constexpr bool enableValidationLayers = false;
|
||||
#else
|
||||
constexpr bool enableValidationLayers = true;
|
||||
#endif
|
||||
|
||||
class Application {
|
||||
public:
|
||||
// FUNCTIONS //
|
||||
|
||||
void run();
|
||||
|
||||
private:
|
||||
// MEMBERS //
|
||||
|
||||
GLFWwindow *m_window = nullptr;
|
||||
vk::raii::Context m_context;
|
||||
vk::raii::Instance m_instance = nullptr;
|
||||
vk::raii::DebugUtilsMessengerEXT m_debugMessenger = nullptr;
|
||||
vk::raii::PhysicalDevice m_physicalDevice = nullptr;
|
||||
vk::raii::Device m_device = nullptr;
|
||||
vk::raii::Queue m_queue = nullptr;
|
||||
uint32_t m_queueIndex;
|
||||
vk::raii::SurfaceKHR m_surface = nullptr;
|
||||
vk::raii::SwapchainKHR m_swapchain = nullptr;
|
||||
std::vector<vk::Image> m_swapchainImages;
|
||||
vk::SurfaceFormatKHR m_swapchainSurfaceFormat;
|
||||
vk::Extent2D m_swapchainExtent;
|
||||
std::vector<vk::raii::ImageView> m_swapchainImageViews;
|
||||
vk::raii::PipelineLayout m_pipelineLayout = nullptr;
|
||||
vk::raii::Pipeline m_graphicsPipeline = nullptr;
|
||||
vk::raii::CommandPool m_commandPool = nullptr;
|
||||
std::vector<vk::raii::CommandBuffer> m_commandBuffers;
|
||||
std::vector<vk::raii::Semaphore> m_presentCompleteSemaphores;
|
||||
std::vector<vk::raii::Semaphore> m_renderFinishedSemaphores;
|
||||
std::vector<vk::raii::Fence> m_inFlightFences;
|
||||
bool m_framebufferResized = false;
|
||||
uint32_t m_frameIndex = 0;
|
||||
|
||||
// FUNCTIONS //
|
||||
|
||||
void initWindow(); // Create the GLFW window
|
||||
void initVulkan(); // Init vulkan and create all the vulkan objects we need
|
||||
void mainLoop(); // Run the main loop
|
||||
void drawFrame(); // Draw a new frame
|
||||
void cleanup(); // Clean up all objects
|
||||
|
||||
// GLFW //
|
||||
static void framebufferResizeCallback(GLFWwindow *window, int width, int height);
|
||||
|
||||
// VULKAN //
|
||||
|
||||
// Instance //
|
||||
void createInstance();
|
||||
std::vector<const char *> getRequiredExtensions();
|
||||
void setupDebugMessenger();
|
||||
// Surface //
|
||||
void createSurface();
|
||||
// Device //
|
||||
void pickPhysicalDevice();
|
||||
// Logical device //
|
||||
void createLogicalDevice();
|
||||
// Swapchain //
|
||||
vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<vk::SurfaceFormatKHR> &availableFormats);
|
||||
vk::PresentModeKHR chooseSwapPresentMode(const std::vector<vk::PresentModeKHR> &availablePresentModes);
|
||||
vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR &capabilities);
|
||||
static uint32_t chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities);
|
||||
void createSwapChain();
|
||||
void createImageViews();
|
||||
void recreateSwapchain();
|
||||
void cleanupSwapchain();
|
||||
|
||||
// Shader //
|
||||
static std::vector<char> readFile(const std::string &filename);
|
||||
[[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector<char> &code) const;
|
||||
|
||||
// Pipeline //
|
||||
void createGraphicsPipeline();
|
||||
|
||||
// Commands //
|
||||
void createCommandPool();
|
||||
void createCommandBuffer();
|
||||
void recordCommandBuffer(uint32_t imageIndex);
|
||||
void transition_image_layout(uint32_t imageIndex, vk::ImageLayout oldLayout, vk::ImageLayout newLayout,
|
||||
vk::AccessFlags2 srcAccessMask, vk::AccessFlags2 dstAccessMask,
|
||||
vk::PipelineStageFlags2 srcStageMask, vk::PipelineStageFlags2 dstStageMask);
|
||||
|
||||
// Synchronization //
|
||||
void createSyncObjects();
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
void Application::createCommandPool() {
|
||||
// Create the command pool
|
||||
vk::CommandPoolCreateInfo poolInfo{
|
||||
// Theres a few flags we can set here, mostly eResetCommandBuffer and eTransient
|
||||
// eResetCommandBuffer will allow all command buffers to be reset individually, you normally have to reset
|
||||
// all of them together
|
||||
// eTransient hints that command buffers are rerecorded very often and changes memory allocation
|
||||
.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
|
||||
// Make sure we pass in our queue index as well
|
||||
.queueFamilyIndex = m_queueIndex,
|
||||
};
|
||||
|
||||
// Create the command pool
|
||||
m_commandPool = vk::raii::CommandPool(m_device, poolInfo, nullptr);
|
||||
}
|
||||
|
||||
void Application::createCommandBuffer() {
|
||||
// Clear the vector of command buffers
|
||||
m_commandBuffers.clear();
|
||||
// Define the buffers
|
||||
vk::CommandBufferAllocateInfo allocInfo{
|
||||
// Pass in our command pool
|
||||
.commandPool = m_commandPool,
|
||||
// Theres a few options here, ePrimary and eSecondary. ePrimary buffers can be submitted to a queue for
|
||||
// execution and can not be called on by other buffers, but eSecondary can't be executed by a queue but can
|
||||
// by another buffer
|
||||
.level = vk::CommandBufferLevel::ePrimary,
|
||||
// Each frame gets its own command buffer
|
||||
.commandBufferCount = MAX_FRAMES_IN_FLIGHT};
|
||||
|
||||
// Create the buffers
|
||||
m_commandBuffers = vk::raii::CommandBuffers(m_device, allocInfo);
|
||||
}
|
||||
|
||||
void Application::recordCommandBuffer(uint32_t imageIndex) {
|
||||
auto &commandBuffer = m_commandBuffers[m_frameIndex];
|
||||
commandBuffer.begin({});
|
||||
|
||||
// Set the image layout to eColorAttachmentOptimal and access to eColorAttachmentWrite
|
||||
transition_image_layout(imageIndex, vk::ImageLayout::eUndefined, vk::ImageLayout::eColorAttachmentOptimal, {},
|
||||
vk::AccessFlagBits2::eColorAttachmentWrite,
|
||||
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
||||
vk::PipelineStageFlagBits2::eColorAttachmentOutput);
|
||||
|
||||
// Configure the clear color
|
||||
vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
// Configure the attachment info
|
||||
vk::RenderingAttachmentInfo attachmentInfo{.imageView = m_swapchainImageViews[imageIndex],
|
||||
.imageLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
||||
.loadOp = vk::AttachmentLoadOp::eClear,
|
||||
.storeOp = vk::AttachmentStoreOp::eStore,
|
||||
.clearValue = clearColor};
|
||||
// Configure the rendering info
|
||||
vk::RenderingInfo renderingInfo{.renderArea = {.offset = {0, 0}, .extent = m_swapchainExtent},
|
||||
.layerCount = 1,
|
||||
.colorAttachmentCount = 1,
|
||||
.pColorAttachments = &attachmentInfo};
|
||||
|
||||
// Begin rendering
|
||||
commandBuffer.beginRendering(renderingInfo);
|
||||
|
||||
// Set the viewport and scissor sizes *dynamically*
|
||||
commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast<float>(m_swapchainExtent.width),
|
||||
static_cast<float>(m_swapchainExtent.height), 0.0f, 1.0f));
|
||||
commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), m_swapchainExtent));
|
||||
|
||||
// Bind the pipeline we want to use
|
||||
commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, m_graphicsPipeline);
|
||||
// Draw!
|
||||
commandBuffer.draw(3, 1, 0, 0);
|
||||
|
||||
commandBuffer.endRendering();
|
||||
|
||||
// Set the image layout back to ePresentSrcKHR so it can be presented to the screen
|
||||
transition_image_layout(imageIndex, vk::ImageLayout::eColorAttachmentOptimal, vk::ImageLayout::ePresentSrcKHR,
|
||||
vk::AccessFlagBits2::eColorAttachmentWrite, {},
|
||||
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
||||
vk::PipelineStageFlagBits2::eBottomOfPipe);
|
||||
|
||||
commandBuffer.end();
|
||||
}
|
||||
|
||||
void Application::transition_image_layout(uint32_t imageIndex, vk::ImageLayout oldLayout, vk::ImageLayout newLayout,
|
||||
vk::AccessFlags2 srcAccessMask, vk::AccessFlags2 dstAccessMask,
|
||||
vk::PipelineStageFlags2 srcStageMask, vk::PipelineStageFlags2 dstStageMask) {
|
||||
vk::ImageMemoryBarrier2 barrier = {.srcStageMask = srcStageMask,
|
||||
.srcAccessMask = srcAccessMask,
|
||||
.dstStageMask = dstStageMask,
|
||||
.dstAccessMask = dstAccessMask,
|
||||
.oldLayout = oldLayout,
|
||||
.newLayout = newLayout,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = m_swapchainImages[imageIndex],
|
||||
.subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1}};
|
||||
|
||||
vk::DependencyInfo dependencyInfo{
|
||||
.dependencyFlags = {}, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier};
|
||||
|
||||
m_commandBuffers[m_frameIndex].pipelineBarrier2(dependencyInfo);
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
// List of extensions we need
|
||||
std::vector<const char *> requiredDeviceExtensions = {
|
||||
// Make sure we have a swapchain
|
||||
vk::KHRSwapchainExtensionName,
|
||||
// Make sure we can use spirv
|
||||
vk::KHRSpirv14ExtensionName,
|
||||
// Make sure we can do synchronization
|
||||
vk::KHRSynchronization2ExtensionName,
|
||||
// Make sure we can create renderpasses
|
||||
vk::KHRCreateRenderpass2ExtensionName};
|
||||
|
||||
void Application::pickPhysicalDevice() {
|
||||
// Get an array of all device
|
||||
std::vector<vk::raii::PhysicalDevice> devices = m_instance.enumeratePhysicalDevices();
|
||||
// Loop through the devices
|
||||
const auto devIter = std::ranges::find_if(devices, [&](auto const &device) {
|
||||
// Make sure the device supports vulkan 1.4 or higher
|
||||
bool supportsVulkan13 = device.getProperties().apiVersion >= VK_API_VERSION_1_4;
|
||||
|
||||
// Make sure the device supports graphics
|
||||
auto queueFamilies = device.getQueueFamilyProperties();
|
||||
bool supportsGraphics = std::ranges::any_of(
|
||||
queueFamilies, [](auto const &qfp) { return !!(qfp.queueFlags & vk::QueueFlagBits::eGraphics); });
|
||||
|
||||
// Make sure the device supports all the extensions we want to enable
|
||||
auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties();
|
||||
bool supportsAllRequiredExtensions = std::ranges::all_of(
|
||||
requiredDeviceExtensions, [&availableDeviceExtensions](auto const &requiredDeviceExtension) {
|
||||
return std::ranges::any_of(
|
||||
availableDeviceExtensions, [requiredDeviceExtension](auto const &availableDeviceExtension) {
|
||||
return strcmp(availableDeviceExtension.extensionName, requiredDeviceExtension) == 0;
|
||||
});
|
||||
});
|
||||
|
||||
// Make sure the device supports the features we want to enable
|
||||
auto features = device.template getFeatures2<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features,
|
||||
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>();
|
||||
bool supportsRequiredFeatures =
|
||||
features.template get<vk::PhysicalDeviceVulkan13Features>().dynamicRendering &&
|
||||
features.template get<vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>().extendedDynamicState;
|
||||
|
||||
// Return a 1 if everything is supported
|
||||
return supportsVulkan13 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures;
|
||||
});
|
||||
|
||||
// In the case we found a device, set it as our physical device
|
||||
if (devIter != devices.end()) {
|
||||
m_physicalDevice = *devIter;
|
||||
} else {
|
||||
// If we didn't find a device, error out
|
||||
throw std::runtime_error("Failed to find a suitable GPU");
|
||||
}
|
||||
}
|
||||
|
||||
void Application::createLogicalDevice() {
|
||||
// Get the supported queue families from the physical device
|
||||
std::vector<vk::QueueFamilyProperties> queueFamilyProperties = m_physicalDevice.getQueueFamilyProperties();
|
||||
|
||||
// Loop through and get the graphics and present queue
|
||||
m_queueIndex = ~0;
|
||||
for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) {
|
||||
if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) &&
|
||||
m_physicalDevice.getSurfaceSupportKHR(qfpIndex, *m_surface)) {
|
||||
m_queueIndex = qfpIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If a graphics and present queue are not found, error out
|
||||
if (m_queueIndex == ~0) {
|
||||
throw std::runtime_error("Failed to find a queue that supports graphics and presentation");
|
||||
}
|
||||
|
||||
// Define the queue
|
||||
float queuePriority = 0.5;
|
||||
vk::DeviceQueueCreateInfo deviceQueueCreateInfo{
|
||||
// Pass the index of the queue we want to use
|
||||
.queueFamilyIndex = m_queueIndex,
|
||||
.queueCount = 1,
|
||||
.pQueuePriorities = &queuePriority,
|
||||
};
|
||||
|
||||
// We don't need any specific device features at the moment, so just default to
|
||||
// everything being set to false
|
||||
vk::PhysicalDeviceFeatures deviceFeatures;
|
||||
|
||||
// Most vulkan structures have a .pNext value that is used to chain multiple structures
|
||||
// together. Using a structure chain we can do this chaining automatically instead of
|
||||
// having to manually define the .pNext for each element.
|
||||
vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features,
|
||||
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>
|
||||
featureChain = {
|
||||
{}, // PhysicalDeviceFeatures1
|
||||
{.dynamicRendering = true}, // PhysicalDeviceVulkan12Features
|
||||
{.extendedDynamicState = true} // PhysicalDeviceExtendedDynamicStateFeaturesEXT
|
||||
};
|
||||
|
||||
// Define the logical device
|
||||
vk::DeviceCreateInfo deviceCreateInfo{
|
||||
// Make sure we pass the structure chain of the features we want to enable
|
||||
.pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
|
||||
// Pass the queue info
|
||||
.queueCreateInfoCount = 1,
|
||||
.pQueueCreateInfos = &deviceQueueCreateInfo,
|
||||
// Set the logical device extensions
|
||||
.enabledExtensionCount = static_cast<uint32_t>(requiredDeviceExtensions.size()),
|
||||
.ppEnabledExtensionNames = requiredDeviceExtensions.data(),
|
||||
};
|
||||
|
||||
// Create the logical device and queue
|
||||
m_device = vk::raii::Device(m_physicalDevice, deviceCreateInfo);
|
||||
m_queue = vk::raii::Queue(m_device, m_queueIndex, 0);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <vulkan/vk_platform.h>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include <vulkan/vulkan_to_string.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
void Application::createInstance() {
|
||||
// Create the app info
|
||||
constexpr vk::ApplicationInfo appInfo{.pApplicationName = "Vulk",
|
||||
.applicationVersion = VK_MAKE_VERSION(1, 0, 0),
|
||||
.pEngineName = "No Engine",
|
||||
.engineVersion = VK_MAKE_API_VERSION(1, 0, 0, 0),
|
||||
// Define what version of vulkan we want to use
|
||||
.apiVersion = vk::ApiVersion14};
|
||||
|
||||
// Get the validation layers
|
||||
std::vector<char const *> requiredLayers;
|
||||
if (enableValidationLayers) {
|
||||
requiredLayers.assign(validationLayers.begin(), validationLayers.end());
|
||||
}
|
||||
|
||||
// Loop through and make sure all required layers are supported
|
||||
auto layerProperties = m_context.enumerateInstanceLayerProperties();
|
||||
if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const &requiredLayer) {
|
||||
return std::ranges::none_of(layerProperties, [requiredLayer](auto const &layerProperty) {
|
||||
return strcmp(layerProperty.layerName, requiredLayer) == 0;
|
||||
});
|
||||
})) {
|
||||
throw std::runtime_error("One or more required layers not supported");
|
||||
}
|
||||
|
||||
// Get a vector of the extensions we want to enable
|
||||
auto requiredExtensions = getRequiredExtensions();
|
||||
// Get the extensions that are supported
|
||||
auto extensionProperties = m_context.enumerateInstanceExtensionProperties();
|
||||
// Loop through all required extensions
|
||||
for (auto const &requiredExtension: requiredExtensions) {
|
||||
if (std::ranges::none_of(extensionProperties, [requiredExtension](auto const &extensionProperty) {
|
||||
return strcmp(extensionProperty.extensionName, requiredExtension) == 0;
|
||||
})) {
|
||||
throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create the instance crate info
|
||||
vk::InstanceCreateInfo createInfo{// Pass our application info
|
||||
.pApplicationInfo = &appInfo,
|
||||
// Define the layers we want
|
||||
.enabledLayerCount = static_cast<uint32_t>(requiredLayers.size()),
|
||||
.ppEnabledLayerNames = requiredLayers.data(),
|
||||
// Define the extensions we want
|
||||
.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size()),
|
||||
.ppEnabledExtensionNames = requiredExtensions.data()};
|
||||
|
||||
// Create the instance
|
||||
m_instance = vk::raii::Instance(m_context, createInfo);
|
||||
}
|
||||
|
||||
std::vector<const char *> Application::getRequiredExtensions() {
|
||||
// Get the list of extensions that GLFW needs
|
||||
uint32_t glfwExtensionCount = 0;
|
||||
auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
|
||||
// GLFW returns it as a const char** so convert to a vector
|
||||
std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
|
||||
|
||||
// Make sure we include validation layers if enabled
|
||||
if (enableValidationLayers) {
|
||||
extensions.push_back(vk::EXTDebugUtilsExtensionName);
|
||||
}
|
||||
|
||||
// Return the list
|
||||
return extensions;
|
||||
}
|
||||
|
||||
static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT severity,
|
||||
vk::DebugUtilsMessageTypeFlagsEXT type,
|
||||
const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData,
|
||||
void *) {
|
||||
// Simply just print the message to stdout
|
||||
std::cerr << "Validation layer: type " << vk::to_string(type) << " msg: " << pCallbackData->pMessage << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Application::setupDebugMessenger() {
|
||||
// If validation layers are disabled, we can just skip this step
|
||||
if (!enableValidationLayers) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Define what severity we want
|
||||
vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose |
|
||||
vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning |
|
||||
vk::DebugUtilsMessageSeverityFlagBitsEXT::eError);
|
||||
// Define the type of validation messages we want
|
||||
vk::DebugUtilsMessageTypeFlagsEXT typeFlags(vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral |
|
||||
vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation |
|
||||
vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance);
|
||||
// Create the messenger
|
||||
vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo{
|
||||
.messageSeverity = severityFlags, .messageType = typeFlags, .pfnUserCallback = &debugCallback};
|
||||
m_debugMessenger = m_instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfo);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#include <iostream>
|
||||
#include "app.h"
|
||||
|
||||
int main() {
|
||||
// Create the application
|
||||
Application app;
|
||||
|
||||
try {
|
||||
// Run le app
|
||||
app.run();
|
||||
} catch (const std::exception &e) {
|
||||
// If we have an error just print it out and exit
|
||||
std::cerr << e.what() << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#include <cstdint>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
void Application::createGraphicsPipeline() {
|
||||
// Load in the shader
|
||||
auto shaderCode = readFile("assets/shaders/shader.spv");
|
||||
// Create the shader module
|
||||
vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode);
|
||||
|
||||
// Define the fragment and vertex shader stages, making sure to specify "vertMain" and "fragMain" are the entry
|
||||
// points
|
||||
vk::PipelineShaderStageCreateInfo vertShaderStageInfo{
|
||||
.stage = vk::ShaderStageFlagBits::eVertex, .module = shaderModule, .pName = "vertMain"};
|
||||
vk::PipelineShaderStageCreateInfo fragShaderStageInfo{
|
||||
.stage = vk::ShaderStageFlagBits::eFragment, .module = shaderModule, .pName = "fragMain"};
|
||||
// Make sure the stages are in an array so its easier to pass on later on
|
||||
vk::PipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};
|
||||
|
||||
|
||||
// Set the dynamic states. *Normally* pipelines are completely immutable and require full recreation if anything
|
||||
// needs to be changed. Using dynamic states we can change *some* of the pipeline. In this case, the viewport and
|
||||
// scissor are dynamic and defined at frametime
|
||||
std::vector dynamicStates = {vk::DynamicState::eViewport, vk::DynamicState::eScissor};
|
||||
vk::PipelineDynamicStateCreateInfo dynamicState{.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size()),
|
||||
.pDynamicStates = dynamicStates.data()};
|
||||
|
||||
// Normally you'd pass vertices to the GPU requiring you to define how that vertex data is structured. Here we have
|
||||
// hardcoded the vertices into the shader itself to keep stuff somewhat simple. As a result the vertex input state
|
||||
// is just going to be empty.
|
||||
vk::PipelineVertexInputStateCreateInfo vertexInputInfo;
|
||||
|
||||
// Set the vertex assembly
|
||||
vk::PipelineInputAssemblyStateCreateInfo inputAssemblyInfo{
|
||||
.topology = vk::PrimitiveTopology::eTriangleList,
|
||||
};
|
||||
|
||||
// Define out the viewports and scissors
|
||||
vk::PipelineViewportStateCreateInfo viewportState{
|
||||
.viewportCount = 1, .pViewports = {}, .scissorCount = 1, .pScissors = {}};
|
||||
|
||||
// Define the rasterization state
|
||||
vk::PipelineRasterizationStateCreateInfo rasterizationStateInfo{
|
||||
// If we want to clamp the depth buffer data to a range of values
|
||||
.depthClampEnable = vk::False,
|
||||
// If we want to allow fragment discarding
|
||||
.rasterizerDiscardEnable = vk::False,
|
||||
// How we want to draw polygons, fill, lines, points, etc
|
||||
.polygonMode = vk::PolygonMode::eFill,
|
||||
// Face culling
|
||||
.cullMode = vk::CullModeFlagBits::eBack,
|
||||
.frontFace = vk::FrontFace::eClockwise,
|
||||
// Depth buffer data doesn't have to be a linear 0.0f-1.0f, here we can bias it
|
||||
.depthBiasEnable = vk::False,
|
||||
.depthBiasSlopeFactor = 1.0f,
|
||||
// Line width
|
||||
.lineWidth = 1.0f};
|
||||
|
||||
// This is for MSAA, at the moment we aren't using it but should be fairly simple to enable, just need to change the
|
||||
// swapchain images to support it, change the sample count here, and let GLFW know.
|
||||
vk::PipelineMultisampleStateCreateInfo multisampling{.rasterizationSamples = vk::SampleCountFlagBits::e1,
|
||||
.sampleShadingEnable = vk::False};
|
||||
|
||||
// Color blending attachment
|
||||
vk::PipelineColorBlendAttachmentState colorBlendAttachment{
|
||||
// Enabled or not
|
||||
.blendEnable = vk::False,
|
||||
// Set the src and dst color factors
|
||||
.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha,
|
||||
.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha,
|
||||
// Set the color operation
|
||||
.colorBlendOp = vk::BlendOp::eAdd,
|
||||
// Set the src and dst alpha factors
|
||||
.srcAlphaBlendFactor = vk::BlendFactor::eOne,
|
||||
.dstAlphaBlendFactor = vk::BlendFactor::eZero,
|
||||
// Set the alpha operation
|
||||
.alphaBlendOp = vk::BlendOp::eAdd,
|
||||
// Define what components we want to blend
|
||||
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
|
||||
|
||||
// Color blending as a whole
|
||||
vk::PipelineColorBlendStateCreateInfo colorBlending{// Enabled or not
|
||||
.logicOpEnable = vk::False,
|
||||
// Operation to run between attachments
|
||||
.logicOp = vk::LogicOp::eCopy,
|
||||
// Our attachment/s
|
||||
.attachmentCount = 1,
|
||||
.pAttachments = &colorBlendAttachment};
|
||||
|
||||
// Pipeline layout
|
||||
vk::PipelineLayoutCreateInfo pipelineLayoutInfo{.setLayoutCount = 0, .pushConstantRangeCount = 0};
|
||||
|
||||
// Create the layout
|
||||
m_pipelineLayout = vk::raii::PipelineLayout{m_device, pipelineLayoutInfo};
|
||||
|
||||
// Set the rending state
|
||||
vk::PipelineRenderingCreateInfo renderingCreateInfo{.colorAttachmentCount = 1,
|
||||
.pColorAttachmentFormats = &m_swapchainSurfaceFormat.format};
|
||||
|
||||
// Create the pipeline
|
||||
// (could use a structure chain here if i wanted...)
|
||||
vk::GraphicsPipelineCreateInfo pipelineInfo{
|
||||
// Make sure we include the rendering create info next
|
||||
.pNext = &renderingCreateInfo,
|
||||
// Specify the stages
|
||||
.stageCount = 2,
|
||||
.pStages = shaderStages,
|
||||
// Specify the vertex input
|
||||
.pVertexInputState = &vertexInputInfo,
|
||||
// Specify the vertex assembly
|
||||
.pInputAssemblyState = &inputAssemblyInfo,
|
||||
// Specify the viewport state
|
||||
.pViewportState = &viewportState,
|
||||
// Specify the rasterization state
|
||||
.pRasterizationState = &rasterizationStateInfo,
|
||||
// Specify the MSAA state
|
||||
.pMultisampleState = &multisampling,
|
||||
// Set blending
|
||||
.pColorBlendState = &colorBlending,
|
||||
// Set the dynamic states
|
||||
.pDynamicState = &dynamicState,
|
||||
// Define the layout
|
||||
.layout = m_pipelineLayout,
|
||||
// Normally you'd need to define a render pass, however, since we are doing some dynamic state stuff we
|
||||
// don't need to
|
||||
.renderPass = nullptr,
|
||||
// These are here for pipeline creation. Since you'd likely need many many pipelines as they are immutable,
|
||||
// vulkan allows you to pass an already created pipeline and base your new pipeline on it.
|
||||
.basePipelineHandle = VK_NULL_HANDLE, // Optional for now, but useful to remember in the future
|
||||
.basePipelineIndex = -1 // Same here...
|
||||
};
|
||||
|
||||
// Create the pipeline
|
||||
m_graphicsPipeline = vk::raii::Pipeline(m_device, nullptr, pipelineInfo, nullptr);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <fstream>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
std::vector<char> Application::readFile(const std::string &filename) {
|
||||
// Load the file in, checking to make sure it openned without errors
|
||||
std::ifstream file(filename, std::ios::ate | std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Failed to open file: " + filename);
|
||||
}
|
||||
|
||||
// Create a buffer with the same size of the file
|
||||
std::vector<char> buffer(file.tellg());
|
||||
// Make sure we are at the beginning of the file
|
||||
file.seekg(0, std::ios::beg);
|
||||
// Read the data in
|
||||
file.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
|
||||
// Close the file and return the buffer
|
||||
file.close();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
vk::raii::ShaderModule Application::createShaderModule(const std::vector<char> &code) const {
|
||||
// Create the shader module (feel like this is self explainatory enough to not need a comment at all...)
|
||||
vk::ShaderModuleCreateInfo createInfo{.codeSize = code.size() * sizeof(char),
|
||||
.pCode = reinterpret_cast<const uint32_t *>(code.data())};
|
||||
|
||||
vk::raii::ShaderModule shaderModule{m_device, createInfo};
|
||||
|
||||
return shaderModule;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
vk::SurfaceFormatKHR Application::chooseSwapSurfaceFormat(const std::vector<vk::SurfaceFormatKHR> &availableFormats) {
|
||||
// Check for eB8G8R8Srgb & eSrgbNonlinear support
|
||||
for (const auto &availableFormat: availableFormats) {
|
||||
if (availableFormat.format == vk::Format::eB8G8R8Srgb &&
|
||||
availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) {
|
||||
return availableFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// If eB8G8R8Srgb & eSrgbNonlinear are not available, default to the first available format
|
||||
return availableFormats[0];
|
||||
}
|
||||
|
||||
vk::PresentModeKHR Application::chooseSwapPresentMode(const std::vector<vk::PresentModeKHR> &availablePresentModes) {
|
||||
// Check for eMailbox support. Return that if available.
|
||||
for (const auto &availablePresentMode: availablePresentModes) {
|
||||
if (availablePresentMode == vk::PresentModeKHR::eMailbox) {
|
||||
return availablePresentMode;
|
||||
}
|
||||
}
|
||||
|
||||
// eFifo is guaranteed to be supported, so default to it.
|
||||
return vk::PresentModeKHR::eFifo;
|
||||
}
|
||||
|
||||
vk::Extent2D Application::chooseSwapExtent(const vk::SurfaceCapabilitiesKHR &capabilities) {
|
||||
// If the extent is already created, just use that.
|
||||
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
|
||||
return capabilities.currentExtent;
|
||||
}
|
||||
|
||||
// Get the size of the window
|
||||
int width, height;
|
||||
glfwGetWindowSize(m_window, &width, &height);
|
||||
// Return a new extent, clamping the window size between the min and max image extents
|
||||
return {
|
||||
std::clamp<uint32_t>(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
|
||||
std::clamp<uint32_t>(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height),
|
||||
};
|
||||
}
|
||||
|
||||
uint32_t Application::chooseSwapMinImageCount(vk::SurfaceCapabilitiesKHR const &surfaceCapabilities) {
|
||||
// Clamp to the min image count or 3 if min is less.
|
||||
auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount);
|
||||
// There are cases where a GPU doesn't support images (servers) or its max count is below 3, so clamp to that.
|
||||
if ((0 < surfaceCapabilities.maxImageCount) && (surfaceCapabilities.maxImageCount < minImageCount)) {
|
||||
minImageCount = surfaceCapabilities.maxImageCount;
|
||||
}
|
||||
|
||||
return minImageCount;
|
||||
}
|
||||
|
||||
void Application::createSwapChain() {
|
||||
// Get the surfaceCapabilities
|
||||
auto surfaceCapabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_surface);
|
||||
// Choose the surface format
|
||||
m_swapchainSurfaceFormat = chooseSwapSurfaceFormat(m_physicalDevice.getSurfaceFormatsKHR(m_surface));
|
||||
// Figure out the size of the swapchain images
|
||||
m_swapchainExtent = chooseSwapExtent(surfaceCapabilities);
|
||||
|
||||
vk::SwapchainCreateInfoKHR swapchainCreateInfo{
|
||||
// Pass the surface
|
||||
.surface = *m_surface,
|
||||
// Specify the number of images
|
||||
.minImageCount = chooseSwapMinImageCount(surfaceCapabilities),
|
||||
// Pass the image format, colorspace, and size
|
||||
.imageFormat = m_swapchainSurfaceFormat.format,
|
||||
.imageColorSpace = m_swapchainSurfaceFormat.colorSpace,
|
||||
.imageExtent = m_swapchainExtent,
|
||||
// The number of layers (images) in this image, think about how a 3d image is just multiple 2d image layers
|
||||
.imageArrayLayers = 1,
|
||||
// Define the image usage
|
||||
.imageUsage = vk::ImageUsageFlagBits::eColorAttachment,
|
||||
// Define the sharing mode
|
||||
.imageSharingMode = vk::SharingMode::eExclusive,
|
||||
// Transform the images to the current surface
|
||||
.preTransform = surfaceCapabilities.currentTransform,
|
||||
.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
|
||||
// Set the present mode
|
||||
.presentMode = chooseSwapPresentMode(m_physicalDevice.getSurfacePresentModesKHR(*m_surface)),
|
||||
// Clip the images
|
||||
.clipped = true,
|
||||
};
|
||||
|
||||
// Create the swapchain
|
||||
m_swapchain = vk::raii::SwapchainKHR(m_device, swapchainCreateInfo);
|
||||
// Get an array of the swapchain images
|
||||
m_swapchainImages = m_swapchain.getImages();
|
||||
}
|
||||
|
||||
void Application::createImageViews() {
|
||||
// Clear the image views array
|
||||
m_swapchainImageViews.clear();
|
||||
// Create a creatInfo for all image views
|
||||
vk::ImageViewCreateInfo imageViewCreateInfo{// Define the image type
|
||||
.viewType = vk::ImageViewType::e2D,
|
||||
// Define the format (same as the swapchain image format)
|
||||
.format = m_swapchainSurfaceFormat.format,
|
||||
// Define the subresourceRange
|
||||
.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}};
|
||||
|
||||
// Loop through the images in the swapchain and create the m_imageViews
|
||||
for (auto image: m_swapchainImages) {
|
||||
imageViewCreateInfo.image = image;
|
||||
m_swapchainImageViews.emplace_back(m_device, imageViewCreateInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::cleanupSwapchain() {
|
||||
// Clear the image views
|
||||
m_swapchainImageViews.clear();
|
||||
// Set the swapchain as a nullptr (i believe raii will handle the dealloc)
|
||||
m_swapchain = nullptr;
|
||||
}
|
||||
|
||||
void Application::recreateSwapchain() {
|
||||
// We really should only need to recreate the swapchain when the window is resized, in that case we need to get the
|
||||
// new size of the window
|
||||
int width = 0, height = 0;
|
||||
glfwGetFramebufferSize(m_window, &width, &height);
|
||||
while (width == 0 || height == 0) {
|
||||
// In the case the window is minimized, we want to get the size and wait for new events until unminimized
|
||||
glfwGetFramebufferSize(m_window, &width, &height);
|
||||
glfwWaitEvents();
|
||||
}
|
||||
|
||||
// Wait for all current tasks to complete so we don't destroy in use resources
|
||||
m_device.waitIdle();
|
||||
|
||||
// Cleanup and create the swapchain
|
||||
cleanupSwapchain();
|
||||
createSwapChain();
|
||||
createImageViews();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
#include "vulkan/vulkan.hpp"
|
||||
|
||||
// A note on semaphores and fences:
|
||||
// For a long while these honestly scared me a bit. In hind sight I have no idea why, maybe cause (for me atleast) they
|
||||
// weren't all that intuitive or it was just explained wrong or something. Either way, they are a fairly simple and very
|
||||
// very powerful. I'm going to *attempt* to explain them as well as I can with what and how I know them so far. Should
|
||||
// be said that at this stage I am still very much in the process of learning and understanding vulkan and at its
|
||||
// current state this is just still a rainbow triangle. If I do a shit job heres the documentation that finally made it
|
||||
// click for me. Granted most of what is there is described here.
|
||||
// https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.html#_synchronization
|
||||
//
|
||||
// Semaphores:
|
||||
// At their core Semaphores are just a boolean that is set when a GPU task is finished and checked (if passed in the
|
||||
// tasks create info). With it being a boolean, there are two states it can be set as, signaled or unsignaled. All
|
||||
// semaphores start life as unsignaled.
|
||||
//
|
||||
// Say you have 2 tasks, A and B, and a single semaphore, S. You want B to execute on the GPU only when task A is
|
||||
// finished. In this case we want task A to signal S and task B to wait on S.
|
||||
// In pseudocode this would sort of look like below.
|
||||
//
|
||||
// vkQueueSubmit(work: A, signal: S, wait: None); // Submit work A, signaling S when done
|
||||
// vkQueueSubmit(work: B, signal: None, wait: S); // Submit work B, waiting for S to signal
|
||||
//
|
||||
// Fences:
|
||||
// Fences work much the same but on the CPU side. Just like semaphores at there core they are booleans. But they can be
|
||||
// used to signal the CPU when a GPU task is finished. This allows you to have the CPU wait for GPU tasks to complete
|
||||
// instead of just having all task return instantly.
|
||||
//
|
||||
// In this case say you have task A and fence F,
|
||||
//
|
||||
// vkQueueSubmit(work: A, fence: F); // Submit A to the GPU, returns immediately, but signals fence F when done.
|
||||
// vkWaitForFence(F); // Wait for fence F to signal
|
||||
// someOtherFunctionToRunAfterTheTask(); // Run some task afterwards
|
||||
//
|
||||
// Notes:
|
||||
// Most importantly a fence will block execution on the host (CPU) while a call with a semaphore will always return
|
||||
// whether or not the task has finished.
|
||||
//
|
||||
// While I've compared semaphores and fences to booleans, there are other flags that change the behavior that I have yet
|
||||
// to explore. While I feel like for the most part, you can get away with using semaphores as described, you might be
|
||||
// able to use them in a different way and have much much better results. Always check the documentation.
|
||||
//
|
||||
// Semaphores:
|
||||
// https://docs.vulkan.org/refpages/latest/refpages/source/VkSemaphore.html
|
||||
// Fences:
|
||||
// https://docs.vulkan.org/refpages/latest/refpages/source/VkFence.html
|
||||
//
|
||||
// Other documentation:
|
||||
// https://www.khronos.org/blog/understanding-vulkan-synchronization
|
||||
|
||||
|
||||
void Application::createSyncObjects() {
|
||||
// The last thing we want to do is accidentally re-create the sync objects while they are in use. Ideally this
|
||||
// function is called once on init, but programming is hard so we should error out if the vectors aren't empty.
|
||||
assert(m_presentCompleteSemaphores.empty() && m_renderFinishedSemaphores.empty() && m_inFlightFences.empty() &&
|
||||
"Attempt to create sync objects while vectors are non-empty");
|
||||
|
||||
// Create the m_renderFinishedSemaphores
|
||||
for (size_t i = 0; i < m_swapchainImages.size(); i++) {
|
||||
m_renderFinishedSemaphores.emplace_back(m_device, vk::SemaphoreCreateInfo());
|
||||
}
|
||||
|
||||
// Create the m_presentCompleteSemaphores and m_inFlightFences
|
||||
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
|
||||
m_presentCompleteSemaphores.emplace_back(m_device, vk::SemaphoreCreateInfo());
|
||||
m_inFlightFences.emplace_back(m_device, vk::FenceCreateInfo{.flags = vk::FenceCreateFlagBits::eSignaled});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#include <stdexcept>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
#include <vulkan/vulkan_raii.hpp>
|
||||
#include "app.h"
|
||||
|
||||
void Application::initWindow() {
|
||||
// GLFW is a state machine that needs to be initialized
|
||||
glfwInit();
|
||||
|
||||
// By default, GLFW will spawn an OpenGL client. Since we are using Vulkan, we need to tell it to just not use an
|
||||
// api at all as we will be getting the window surface directly.
|
||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
|
||||
|
||||
// Create the window, checking if it is still a nullptr afterwards to make sure it was created correctly.
|
||||
m_window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
|
||||
if (m_window == nullptr) {
|
||||
// Get the error code and message from GLFW
|
||||
const char *desc;
|
||||
uint32_t code = glfwGetError(&desc);
|
||||
throw std::runtime_error(std::format("Failed to create GLFW window: ({}) {}", code, desc));
|
||||
}
|
||||
|
||||
// GLFW callback functions are required to be static, meaning we can't interact with our Application data directly.
|
||||
// To get around this we can specify a window user pointer (void*) to our application instance and reinterpret_cast
|
||||
// it into an application in the callbacks.
|
||||
glfwSetWindowUserPointer(m_window, this);
|
||||
// Set our framebuffer resize callback
|
||||
glfwSetFramebufferSizeCallback(m_window, framebufferResizeCallback);
|
||||
}
|
||||
|
||||
void Application::framebufferResizeCallback(GLFWwindow *window, int width, int height) {
|
||||
// Get our application user point from the window and reinterpret_cast it.
|
||||
auto app = reinterpret_cast<Application *>(glfwGetWindowUserPointer(window));
|
||||
// Set m_framebufferResized so the swapchain is recreated in the next frame.
|
||||
app->m_framebufferResized = true;
|
||||
}
|
||||
|
||||
void Application::createSurface() {
|
||||
// GLFW uses the C only vulkan types so we need to create a temp surface object.
|
||||
VkSurfaceKHR _surface;
|
||||
// Get the window surface
|
||||
if (glfwCreateWindowSurface(*m_instance, m_window, nullptr, &_surface) != VK_SUCCESS) {
|
||||
throw std::runtime_error("Failed to create window surface");
|
||||
}
|
||||
|
||||
// Remake the surface as a CPP type using the C object GLFW gave us.
|
||||
m_surface = vk::raii::SurfaceKHR(m_instance, _surface);
|
||||
}
|
||||
Reference in New Issue
Block a user