commit 82acaafaacc8736cdace5576089ec6a2803a7cbf Author: firewire Date: Tue Feb 3 18:26:14 2026 -0500 Project diff --git a/.clang-format b/.clang-format new file mode 100755 index 0000000..3c65f56 --- /dev/null +++ b/.clang-format @@ -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 +... diff --git a/.clangd b/.clangd new file mode 100755 index 0000000..adedb95 --- /dev/null +++ b/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Add: -Wno-unknown-warning-option + Remove: [-m*, -f*] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6caa1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +.idea +.zed +!deps/JoltPhysics/Build +.cache +.vscode +build-clion/ \ No newline at end of file diff --git a/.p4ignore b/.p4ignore new file mode 100755 index 0000000..b195081 --- /dev/null +++ b/.p4ignore @@ -0,0 +1,6 @@ +build/ +.idea +.zed +!deps/JoltPhysics/Build +.cache +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 0000000..57b4d34 --- /dev/null +++ b/CMakeLists.txt @@ -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 +) diff --git a/assets/shaders/shader.slang b/assets/shaders/shader.slang new file mode 100644 index 0000000..b0959ca --- /dev/null +++ b/assets/shaders/shader.slang @@ -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); + } diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..f30e8d4 --- /dev/null +++ b/cmake/CPM.cmake @@ -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}) diff --git a/cmake/GLFW.cmake b/cmake/GLFW.cmake new file mode 100644 index 0000000..5b76e39 --- /dev/null +++ b/cmake/GLFW.cmake @@ -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" +) diff --git a/cmake/Vulkan.cmake b/cmake/Vulkan.cmake new file mode 100755 index 0000000..23064ac --- /dev/null +++ b/cmake/Vulkan.cmake @@ -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() + diff --git a/cmake/compileCommands.cmake b/cmake/compileCommands.cmake new file mode 100644 index 0000000..38fa25b --- /dev/null +++ b/cmake/compileCommands.cmake @@ -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() diff --git a/cmake/glsl.cmake b/cmake/glsl.cmake new file mode 100755 index 0000000..6b89d39 --- /dev/null +++ b/cmake/glsl.cmake @@ -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}" +) diff --git a/cmake/platform.cmake b/cmake/platform.cmake new file mode 100644 index 0000000..0791c94 --- /dev/null +++ b/cmake/platform.cmake @@ -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() diff --git a/cmake/slang.cmake b/cmake/slang.cmake new file mode 100755 index 0000000..9ce9b92 --- /dev/null +++ b/cmake/slang.cmake @@ -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}" +) diff --git a/scripts/build-debug.sh b/scripts/build-debug.sh new file mode 100755 index 0000000..497b992 --- /dev/null +++ b/scripts/build-debug.sh @@ -0,0 +1,8 @@ +#/bin/bash + +cd .. +mkdir -p build +cd build +mkdir -p debug +cd debug +ninja diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..01fa218 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,8 @@ +#/bin/bash + +cd .. +mkdir -p build +cd build +mkdir -p release +cd release +ninja diff --git a/scripts/build-relwithdeb.sh b/scripts/build-relwithdeb.sh new file mode 100755 index 0000000..396dca8 --- /dev/null +++ b/scripts/build-relwithdeb.sh @@ -0,0 +1,8 @@ +#/bin/bash + +cd .. +mkdir -p build +cd build +mkdir -p relwithdeb +cd relwithdeb +ninja diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..627c40e --- /dev/null +++ b/scripts/build.sh @@ -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 diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 0000000..3595225 --- /dev/null +++ b/scripts/clean.sh @@ -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 diff --git a/scripts/configure-debug.sh b/scripts/configure-debug.sh new file mode 100755 index 0000000..e269ff2 --- /dev/null +++ b/scripts/configure-debug.sh @@ -0,0 +1,8 @@ +#/bin/bash + +cd .. +mkdir -p build +cd build +mkdir -p debug +cd debug +cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=Debug diff --git a/scripts/configure-release.sh b/scripts/configure-release.sh new file mode 100755 index 0000000..e6a1d81 --- /dev/null +++ b/scripts/configure-release.sh @@ -0,0 +1,8 @@ +#/bin/bash + +cd .. +mkdir -p build +cd build +mkdir -p release +cd release +cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=Release diff --git a/scripts/configure-relwithdeb.sh b/scripts/configure-relwithdeb.sh new file mode 100755 index 0000000..fa79d5b --- /dev/null +++ b/scripts/configure-relwithdeb.sh @@ -0,0 +1,8 @@ +#/bin/bash + +cd .. +mkdir -p build +cd build +mkdir -p relwithdeb +cd relwithdeb +cmake ../../ -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo diff --git a/src/app.cpp b/src/app.cpp new file mode 100644 index 0000000..bcf5103 --- /dev/null +++ b/src/app.cpp @@ -0,0 +1,136 @@ +#include "app.h" +#include +#include +#include +#include +#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(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(); +} diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..a2b8ef2 --- /dev/null +++ b/src/app.h @@ -0,0 +1,124 @@ + +// INCLUDES // + +// Include vulkan +#include +#include + +#include "vulkan/vulkan.hpp" +#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES) +#include +#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 +#include + +// 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 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 m_swapchainImages; + vk::SurfaceFormatKHR m_swapchainSurfaceFormat; + vk::Extent2D m_swapchainExtent; + std::vector m_swapchainImageViews; + vk::raii::PipelineLayout m_pipelineLayout = nullptr; + vk::raii::Pipeline m_graphicsPipeline = nullptr; + vk::raii::CommandPool m_commandPool = nullptr; + std::vector m_commandBuffers; + std::vector m_presentCompleteSemaphores; + std::vector m_renderFinishedSemaphores; + std::vector 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 getRequiredExtensions(); + void setupDebugMessenger(); + // Surface // + void createSurface(); + // Device // + void pickPhysicalDevice(); + // Logical device // + void createLogicalDevice(); + // Swapchain // + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector &availableFormats); + vk::PresentModeKHR chooseSwapPresentMode(const std::vector &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 readFile(const std::string &filename); + [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector &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(); +}; diff --git a/src/commands.cpp b/src/commands.cpp new file mode 100644 index 0000000..369b682 --- /dev/null +++ b/src/commands.cpp @@ -0,0 +1,110 @@ +#include +#include +#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(m_swapchainExtent.width), + static_cast(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); +} diff --git a/src/device.cpp b/src/device.cpp new file mode 100644 index 0000000..a11be9a --- /dev/null +++ b/src/device.cpp @@ -0,0 +1,123 @@ +#include +#include +#include +#include +#include +#include +#include +#include "app.h" +#include "vulkan/vulkan.hpp" + +// List of extensions we need +std::vector 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 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(); + bool supportsRequiredFeatures = + features.template get().dynamicRendering && + features.template get().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 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 + 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(), + // Pass the queue info + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &deviceQueueCreateInfo, + // Set the logical device extensions + .enabledExtensionCount = static_cast(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); +} diff --git a/src/instance.cpp b/src/instance.cpp new file mode 100644 index 0000000..77764f6 --- /dev/null +++ b/src/instance.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 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(requiredLayers.size()), + .ppEnabledLayerNames = requiredLayers.data(), + // Define the extensions we want + .enabledExtensionCount = static_cast(requiredExtensions.size()), + .ppEnabledExtensionNames = requiredExtensions.data()}; + + // Create the instance + m_instance = vk::raii::Instance(m_context, createInfo); +} + +std::vector 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); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0fde943 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,17 @@ +#include +#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; +} diff --git a/src/pipeline.cpp b/src/pipeline.cpp new file mode 100644 index 0000000..d85455c --- /dev/null +++ b/src/pipeline.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#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(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); +} diff --git a/src/shader.cpp b/src/shader.cpp new file mode 100644 index 0000000..8a9ad12 --- /dev/null +++ b/src/shader.cpp @@ -0,0 +1,32 @@ +#include +#include +#include "app.h" +#include "vulkan/vulkan.hpp" + +std::vector 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 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(buffer.size())); + // Close the file and return the buffer + file.close(); + return buffer; +} + +vk::raii::ShaderModule Application::createShaderModule(const std::vector &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(code.data())}; + + vk::raii::ShaderModule shaderModule{m_device, createInfo}; + + return shaderModule; +} diff --git a/src/swapchain.cpp b/src/swapchain.cpp new file mode 100644 index 0000000..7d8d842 --- /dev/null +++ b/src/swapchain.cpp @@ -0,0 +1,142 @@ +#include +#include +#include +#include +#include +#include "app.h" +#include "vulkan/vulkan.hpp" + +vk::SurfaceFormatKHR Application::chooseSwapSurfaceFormat(const std::vector &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 &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::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(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(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(); +} diff --git a/src/sync.cpp b/src/sync.cpp new file mode 100644 index 0000000..87aa632 --- /dev/null +++ b/src/sync.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#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}); + } +} diff --git a/src/window.cpp b/src/window.cpp new file mode 100644 index 0000000..d4c7677 --- /dev/null +++ b/src/window.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#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(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); +}