This commit is contained in:
2026-02-03 18:26:14 -05:00
commit 82acaafaac
32 changed files with 1468 additions and 0 deletions
Executable
+57
View File
@@ -0,0 +1,57 @@
---
Language: Cpp
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignOperands: true
AlignTrailingComments: false
AlwaysBreakTemplateDeclarations: Yes
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBraces: Custom
BreakConstructorInitializers: AfterColon
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 120
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ContinuationIndentWidth: 8
IncludeCategories:
- Regex: '^<.*'
Priority: 1
- Regex: '^".*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseLabels: true
IndentWidth: 4
InsertNewlineAtEOF: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 2
NamespaceIndentation: All
SpaceAfterCStyleCast: true
SpaceAfterTemplateKeyword: false
SpaceBeforeRangeBasedForLoopColon: false
SpaceInEmptyParentheses: false
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
TabWidth: 4
...
Executable
+3
View File
@@ -0,0 +1,3 @@
CompileFlags:
Add: -Wno-unknown-warning-option
Remove: [-m*, -f*]
+7
View File
@@ -0,0 +1,7 @@
build/
.idea
.zed
!deps/JoltPhysics/Build
.cache
.vscode
build-clion/
Executable
+6
View File
@@ -0,0 +1,6 @@
build/
.idea
.zed
!deps/JoltPhysics/Build
.cache
.vscode
Executable
+42
View File
@@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 4.1)
project(cheesesandwich)
set(CMAKE_CXX_STANDARD 20)
##### Project settings ######
# Set the assets directory
set(ASSETS_DIR "assets")
set(SHADERS_DIR "assets/shaders")
if(${CMAKE_BUILD_TYPE} STREQUAL "Debug")
set(BUILD_SHARED_LIBS ON)
else()
set(BUILD_SHARED_LIBS OFF)
endif()
##### Dependencies #####
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
option(ENABLE_CPP20_MODULE "Enable C++ 20 module support for Vulkan" OFF)
file(GLOB CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake/*.cmake")
foreach(cmake_file ${CMAKE_FILES})
include(${cmake_file})
endforeach()
##### Project actions #####
# Define out executable
file(GLOB SRC_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
file(GLOB HEADER_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
add_executable(${PROJECT_NAME} ${SRC_FILES} ${HEADER_FILES})
# Add the shaders dependency
add_dependencies(${PROJECT_NAME} slang_shaders)
add_dependencies(${PROJECT_NAME} glsl_shaders)
target_include_directories(${PROJECT_NAME} PUBLIC "${glfw_SOURCE_DIR}/include")
# Link with libs
target_link_libraries(${PROJECT_NAME}
glfw
Vulkan::cppm
)
+30
View File
@@ -0,0 +1,30 @@
static float2 positions[3] = float2[] (
float2(0.0, -0.5),
float2(0.5, 0.5),
float2(-0.5, 0.5)
);
static float3 colors[3] = float3[](
float3(1.0, 0.0, 0.0),
float3(0.0, 1.0, 0.0),
float3(0.0, 0.0, 1.0)
);
struct VertexOutput {
float3 color;
float4 sv_position : SV_Position;
};
[shader("vertex")]
VertexOutput vertMain(uint vid: SV_VertexID) {
VertexOutput output;
output.sv_position = float4(positions[vid], 0.0, 1.0);
output.color = colors[vid];
return output;
}
[shader("fragment")]
float4 fragMain(VertexOutput inVert) : SV_Target {
float3 color = inVert.color;
return float4(color, 1.0);
}
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: MIT
#
# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors
set(CPM_DOWNLOAD_VERSION 0.42.1)
set(CPM_HASH_SUM "f3a6dcc6a04ce9e7f51a127307fa4f699fb2bade357a8eb4c5b45df76e1dc6a5")
if(CPM_SOURCE_CACHE)
set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
elseif(DEFINED ENV{CPM_SOURCE_CACHE})
set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
else()
set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
endif()
# Expand relative path. This is important if the provided path contains a tilde (~)
get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE)
file(DOWNLOAD
https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake
${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}
)
include(${CPM_DOWNLOAD_LOCATION})
+11
View File
@@ -0,0 +1,11 @@
# add check for CPM
CPMAddPackage (
URI "https://github.com/glfw/glfw/releases/download/3.4/glfw-3.4.zip"
OPTIONS "GLFW_BUILD_DOCS OFF"
OPTIONS "GLFW_BUILD_TEST OFF"
OPTIONS "GLFW_BUILD_EXAMPLES OFF"
OPTIONS "GLFW_BUILD_X11 OFF"
OPTIONS "GLFW_BUILD_WAYLAND ON"
)
+57
View File
@@ -0,0 +1,57 @@
# Vulkan #
if(ENABLE_CPP20_MODULE)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
endif()
find_package(Vulkan REQUIRED)
if(ENABLE_CPP20_MODULE)
add_library(VulkanCppModule)
add_library(Vulkan::cppm ALIAS VulkanCppModule)
target_compile_definitions(VulkanCppModule
PUBLIC VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1
)
target_include_directories(VulkanCppModule
PUBLIC
"${Vulkan_INCLUDE_DIR}"
)
target_link_libraries(VulkanCppModule
PUBLIC
Vulkan::Vulkan
)
set_target_properties(VulkanCppModule PROPERTIES CXX_STANDARD 20)
if(MSVC)
target_compile_options(VulkanCppModule PRIVATE
/std:c++latest
/permissive-
/Zc:__cplusplus
/EHsc
/Zc:preprocessor
/translateInclude
)
endif()
target_sources(VulkanCppModule
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES
BASE_DIRS
"${Vulkan_INCLUDE_DIR}"
FILES
"${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm"
)
target_sources(VulkanCppModule
PRIVATE
"${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm"
)
else()
add_library(VulkanCppModule INTERFACE)
add_library(Vulkan::cppm ALIAS VulkanCppModule)
target_link_libraries(VulkanCppModule INTERFACE Vulkan::Vulkan)
target_compile_definitions(VulkanCppModule
INTERFACE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 VULKAN_HPP_NO_STRUCT_CONSTRUCTORS=1
)
endif()
+14
View File
@@ -0,0 +1,14 @@
##### SETUP COMPILE COMMANDS #####
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_EXPORT_COMPILE_COMMANDS on)
endif()
if(EXISTS "${CMAKE_BINARY_DIR}/compile_commands.json")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
file(
COPY "${CMAKE_BINARY_DIR}/compile_commands.json"
DESTINATION "${CMAKE_BINARY_DIR}/../"
)
endif()
endif()
+37
View File
@@ -0,0 +1,37 @@
option(SHADERS_DIR "Directory to look for GLSL shaders" "assets/shaders")
find_program(GLSLC_EXECUTABLE NAMES glslc)
if(NOT GLSLC_EXECUTABLE)
message(FATAL_ERROR "glslc executable not found.")
else()
message(STATUS "Found glslc executable: ${GLSLC_EXECUTABLE}")
endif()
function(create_glslc_shader_target TARGET SOURCE_DIR OUTPUT_DIR)
file(MAKE_DIRECTORY ${OUTPUT_DIR})
message(STATUS "GLSL Shader output directory: ${OUTPUT_DIR}")
file (GLOB SHADER_FILES "${SOURCE_DIR}/*.vert")
file(GLOB FRAG_FILES "${SOURCE_DIR}/*.frag")
list(APPEND SHADER_FILES ${FRAG_FILES})
message(STATUS "Found glsl shaders: ${SHADER_FILES}")
set(OUTPUT_FILES "")
foreach(SHADER_FILE ${SHADER_FILES})
get_filename_component(SHADER_NAME ${SHADER_FILE} NAME)
set(OUTPUT_FILE "${OUTPUT_DIR}/${SHADER_NAME}.spv")
list(APPEND OUTPUT_FILES ${OUTPUT_FILE})
message(STATUS "Compiling ${SHADER_FILE} to ${OUTPUT_FILE}")
add_custom_command (
OUTPUT ${OUTPUT_FILE}
COMMAND ${GLSLC_EXECUTABLE} ${SHADER_FILE} -o ${OUTPUT_FILE}
DEPENDS ${SHADER_FILE}
COMMENT "Compiling ${SHADER_FILE} -> ${OUTPUT_FILE}"
VERBATIM
)
endforeach()
add_custom_target(${TARGET} ALL DEPENDS ${OUTPUT_FILES})
endfunction()
create_glslc_shader_target(glsl_shaders
"${SHADERS_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}/${SHADERS_DIR}"
)
+10
View File
@@ -0,0 +1,10 @@
message("System name: ${CMAKE_HOST_SYSTEM_NAME}")
if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows")
add_compile_definitions(PLATFORM_WINDOWS)
elseif(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Linux")
add_compile_definitions(PLATFORM_LINUX)
elseif(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Darwin")
add_compile_definitions(PLATFORM_MACOS)
else()
add_compile_definitions(PLATFORM_UNKNOWN)
endif()
+36
View File
@@ -0,0 +1,36 @@
option(SHADERS_DIR "Directory to look for SLANG shaders" "assets/shaders")
find_program(SLANGC_EXECUTABLE NAMES slangc)
if(NOT SLANGC_EXECUTABLE)
message(FATAL_ERROR "slangc executable not found.")
else()
message(STATUS "Found slangc executable: ${SLANGC_EXECUTABLE}")
endif()
function(create_slang_shader_target TARGET SOURCE_DIR OUTPUT_DIR)
file(MAKE_DIRECTORY ${OUTPUT_DIR})
message(STATUS "Shader output directory: ${OUTPUT_DIR}")
file(GLOB SHADER_FILES "${SOURCE_DIR}/*.slang")
message(STATUS "Found shaders: ${SHADER_FILES}")
set(ENTRY_POINTS -entry vertMain -entry fragMain)
set(OUTPUT_FILES "")
foreach(SHADER_FILE ${SHADER_FILES})
get_filename_component(SHADER_NAME ${SHADER_FILE} NAME_WE)
set(OUTPUT_FILE "${OUTPUT_DIR}/${SHADER_NAME}.spv")
list(APPEND OUTPUT_FILES ${OUTPUT_FILE})
message(STATUS "Compiling ${SHADER_FILE} to ${OUTPUT_FILE}")
add_custom_command(
OUTPUT ${OUTPUT_FILE}
COMMAND ${SLANGC_EXECUTABLE} ${SHADER_FILE} -target spirv -profile spirv_1_4 -emit-spirv-directly -fvk-use-entrypoint-name ${ENTRY_POINTS} -o ${OUTPUT_FILE}
DEPENDS ${SHADER_FILE}
COMMENT "Compiling ${SHADER_FILE} -> ${OUTPUT_FILE}"
VERBATIM
)
endforeach()
add_custom_target(${TARGET} ALL DEPENDS ${OUTPUT_FILES})
endfunction()
create_slang_shader_target(slang_shaders
"${SHADERS_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}/${SHADERS_DIR}"
)
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
cd ..
mkdir -p build
cd build
mkdir -p debug
cd debug
ninja
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
cd ..
mkdir -p build
cd build
mkdir -p release
cd release
ninja
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
cd ..
mkdir -p build
cd build
mkdir -p relwithdeb
cd relwithdeb
ninja
+21
View File
@@ -0,0 +1,21 @@
#/bin/bash
cd ../
mkdir -p build
cd build
mkdir -p debug
cd debug
cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=Debug
ninja
cd ..
mkdir -p release
cd release
cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja
cd ..
mkdir -p relwithdeb
cd relwithdeb
cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
ninja
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
cd ..
cd build/debug || exit
ninja clean
cd ..
cd release || exit
ninja clean
cd ..
cd relwithdeb || exit
ninja clean
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
cd ..
mkdir -p build
cd build
mkdir -p debug
cd debug
cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=Debug
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
cd ..
mkdir -p build
cd build
mkdir -p release
cd release
cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=Release
+8
View File
@@ -0,0 +1,8 @@
#/bin/bash
cd ..
mkdir -p build
cd build
mkdir -p relwithdeb
cd relwithdeb
cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
+136
View File
@@ -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();
}
+124
View File
@@ -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();
};
+110
View File
@@ -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
View File
@@ -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);
}
+110
View File
@@ -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);
}
+17
View File
@@ -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;
}
+138
View File
@@ -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);
}
+32
View File
@@ -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;
}
+142
View File
@@ -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();
}
+72
View File
@@ -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});
}
}
+48
View File
@@ -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);
}