From 639f54c7bf5a2e2db3ed721c4ad769613517b757 Mon Sep 17 00:00:00 2001 From: Alexandre Date: Tue, 7 Apr 2026 14:01:28 +0200 Subject: [PATCH] Added depth buffer, but has some flickering --- .envrc | 3 + .gitignore | 1 + CMake/FindKTX.cmake | 106 +++ CMake/FindVulkan.cmake | 937 +++++++++++++++++++ CMake/FindVulkanHpp.cmake | 426 +++++++++ CMake/Findglm.cmake | 133 +++ CMake/Findnlohmann_json.cmake | 154 ++++ CMake/Findstb.cmake | 86 ++ CMake/Findtinygltf.cmake | 162 ++++ CMake/Findtinyobjloader.cmake | 160 ++++ CMakeLists.txt | 202 +++++ compile_shaders.sh | 1 + shaders/main.slang | 34 + shaders/shader.slang | 30 + shaders/slang.spv | Bin 0 -> 896 bytes src/main.cpp | 1593 +++++++++++++++++++++++++++++++++ textures/texture.jpg | Bin 0 -> 77120 bytes 17 files changed, 4028 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 CMake/FindKTX.cmake create mode 100644 CMake/FindVulkan.cmake create mode 100644 CMake/FindVulkanHpp.cmake create mode 100644 CMake/Findglm.cmake create mode 100644 CMake/Findnlohmann_json.cmake create mode 100644 CMake/Findstb.cmake create mode 100644 CMake/Findtinygltf.cmake create mode 100644 CMake/Findtinyobjloader.cmake create mode 100644 CMakeLists.txt create mode 100755 compile_shaders.sh create mode 100644 shaders/main.slang create mode 100644 shaders/shader.slang create mode 100644 shaders/slang.spv create mode 100644 src/main.cpp create mode 100644 textures/texture.jpg diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..7652aae --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +FLAKEPATH="/home/alex/Developer/nix/NixOSDots/dev-flakes" + +use flake ${FLAKEPATH}/vulkan diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/CMake/FindKTX.cmake b/CMake/FindKTX.cmake new file mode 100644 index 0000000..ac6971a --- /dev/null +++ b/CMake/FindKTX.cmake @@ -0,0 +1,106 @@ +# FindKTX.cmake +# +# Finds the KTX library +# +# This will define the following variables +# +# KTX_FOUND +# KTX_INCLUDE_DIRS +# KTX_LIBRARIES +# +# and the following imported targets +# +# KTX::ktx +# + +# Check if we're on Linux - if so, we'll skip the search and directly use FetchContent +if(UNIX AND NOT APPLE) + # On Linux, we assume KTX is not installed and proceed directly to fetching it + set(KTX_FOUND FALSE) +else() + # On non-Linux platforms, try to find KTX using pkg-config first + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_KTX QUIET ktx libktx ktx2 libktx2) + endif() + + # Try to find KTX using standard find_package + find_path(KTX_INCLUDE_DIR + NAMES ktx.h + PATH_SUFFIXES include ktx KTX ktx2 KTX2 + HINTS + ${PC_KTX_INCLUDEDIR} + /usr/include + /usr/local/include + $ENV{KTX_DIR}/include + $ENV{VULKAN_SDK}/include + ${CMAKE_SOURCE_DIR}/external/ktx/include + ) + + find_library(KTX_LIBRARY + NAMES ktx ktx2 libktx libktx2 + PATH_SUFFIXES lib lib64 + HINTS + ${PC_KTX_LIBDIR} + /usr/lib + /usr/lib64 + /usr/local/lib + /usr/local/lib64 + $ENV{KTX_DIR}/lib + $ENV{VULKAN_SDK}/lib + ${CMAKE_SOURCE_DIR}/external/ktx/lib + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(KTX + REQUIRED_VARS KTX_INCLUDE_DIR KTX_LIBRARY + FAIL_MESSAGE "" # Suppress the error message to allow our fallback + ) + + # Debug output if KTX is not found (only on non-Linux platforms) + if(NOT KTX_FOUND) + message(STATUS "KTX include directory search paths: ${PC_KTX_INCLUDEDIR}, /usr/include, /usr/local/include, $ENV{KTX_DIR}/include, $ENV{VULKAN_SDK}/include, ${CMAKE_SOURCE_DIR}/external/ktx/include") + message(STATUS "KTX library search paths: ${PC_KTX_LIBDIR}, /usr/lib, /usr/lib64, /usr/local/lib, /usr/local/lib64, $ENV{KTX_DIR}/lib, $ENV{VULKAN_SDK}/lib, ${CMAKE_SOURCE_DIR}/external/ktx/lib") + endif() +endif() + +if(KTX_FOUND) + set(KTX_INCLUDE_DIRS ${KTX_INCLUDE_DIR}) + set(KTX_LIBRARIES ${KTX_LIBRARY}) + + if(NOT TARGET KTX::ktx) + add_library(KTX::ktx UNKNOWN IMPORTED) + set_target_properties(KTX::ktx PROPERTIES + IMPORTED_LOCATION "${KTX_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${KTX_INCLUDE_DIRS}" + ) + endif() +else() + # If not found, use FetchContent to download and build + include(FetchContent) + + # Only show the message on non-Linux platforms + if(NOT (UNIX AND NOT APPLE)) + message(STATUS "KTX not found, fetching from GitHub...") + endif() + + FetchContent_Declare( + ktx + GIT_REPOSITORY https://github.com/KhronosGroup/KTX-Software.git + GIT_TAG v4.3.1 # Use a specific tag for stability + ) + + # Set options to minimize build time and dependencies + set(KTX_FEATURE_TOOLS OFF CACHE BOOL "Build KTX tools" FORCE) + set(KTX_FEATURE_DOC OFF CACHE BOOL "Build KTX documentation" FORCE) + set(KTX_FEATURE_TESTS OFF CACHE BOOL "Build KTX tests" FORCE) + + FetchContent_MakeAvailable(ktx) + + # Create an alias to match the expected target name + if(NOT TARGET KTX::ktx) + add_library(KTX::ktx ALIAS ktx) + endif() + + set(KTX_FOUND TRUE) +endif() diff --git a/CMake/FindVulkan.cmake b/CMake/FindVulkan.cmake new file mode 100644 index 0000000..55ac153 --- /dev/null +++ b/CMake/FindVulkan.cmake @@ -0,0 +1,937 @@ +# Updates for iOS Copyright (c) 2024, Holochip Inc. +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +FindVulkan +---------- + +.. versionadded:: 3.7 + +Find Vulkan, which is a low-overhead, cross-platform 3D graphics +and computing API. + +Optional COMPONENTS +^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.24 + +This module respects several optional COMPONENTS. +There are corresponding imported targets for each of these. + +``glslc`` + The SPIR-V compiler. + +``glslangValidator`` + The ``glslangValidator`` tool. + +``glslang`` + The SPIR-V generator library. + +``shaderc_combined`` + The static library for Vulkan shader compilation. + +``SPIRV-Tools`` + Tools to process SPIR-V modules. + +``MoltenVK`` + On macOS, an additional component ``MoltenVK`` is available. + +``dxc`` + .. versionadded:: 3.25 + + The DirectX Shader Compiler. + +The ``glslc`` and ``glslangValidator`` components are provided even +if not explicitly requested (for backward compatibility). + +IMPORTED Targets +^^^^^^^^^^^^^^^^ + +This module defines :prop_tgt:`IMPORTED` targets if Vulkan has been found: + +``Vulkan::Vulkan`` + The main Vulkan library. + +``Vulkan::glslc`` + .. versionadded:: 3.19 + + The GLSLC SPIR-V compiler, if it has been found. + +``Vulkan::Headers`` + .. versionadded:: 3.21 + + Provides just Vulkan headers include paths, if found. No library is + included in this target. This can be useful for applications that + load Vulkan library dynamically. + +``Vulkan::glslangValidator`` + .. versionadded:: 3.21 + + The glslangValidator tool, if found. It is used to compile GLSL and + HLSL shaders into SPIR-V. + +``Vulkan::glslang`` + .. versionadded:: 3.24 + + Defined if SDK has the Khronos-reference front-end shader parser and SPIR-V + generator library (glslang). + +``Vulkan::shaderc_combined`` + .. versionadded:: 3.24 + + Defined if SDK has the Google static library for Vulkan shader compilation + (shaderc_combined). + +``Vulkan::SPIRV-Tools`` + .. versionadded:: 3.24 + + Defined if SDK has the Khronos library to process SPIR-V modules + (SPIRV-Tools). + +``Vulkan::MoltenVK`` + .. versionadded:: 3.24 + + Defined if SDK has the Khronos library which implement a subset of Vulkan API + over Apple Metal graphics framework. (MoltenVK). + +``Vulkan::volk`` + .. versionadded:: 3.25 + + Defined if SDK has the Vulkan meta-loader (volk). + +``Vulkan::dxc_lib`` + .. versionadded:: 3.25 + + Defined if SDK has the DirectX shader compiler library. + +``Vulkan::dxc_exe`` + .. versionadded:: 3.25 + + Defined if SDK has the DirectX shader compiler CLI tool. + +Result Variables +^^^^^^^^^^^^^^^^ + +This module defines the following variables: + +``Vulkan_FOUND`` + set to true if Vulkan was found +``Vulkan_INCLUDE_DIRS`` + include directories for Vulkan +``Vulkan_LIBRARIES`` + link against this library to use Vulkan +``Vulkan_VERSION`` + .. versionadded:: 3.23 + + value from ``vulkan/vulkan_core.h`` +``Vulkan_glslc_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the glslc executable. +``Vulkan_glslangValidator_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the glslangValidator executable. +``Vulkan_glslang_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the glslang library. +``Vulkan_shaderc_combined_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the shaderc_combined library. +``Vulkan_SPIRV-Tools_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the SPIRV-Tools library. +``Vulkan_MoltenVK_FOUND`` + .. versionadded:: 3.24 + + True, if the SDK has the MoltenVK library. +``Vulkan_volk_FOUND`` + .. versionadded:: 3.25 + + True, if the SDK has the volk library. + +``Vulkan_dxc_lib_FOUND`` + .. versionadded:: 3.25 + + True, if the SDK has the DirectX shader compiler library. + +``Vulkan_dxc_exe_FOUND`` + .. versionadded:: 3.25 + + True, if the SDK has the DirectX shader compiler CLI tool. + + +The module will also defines these cache variables: + +``Vulkan_INCLUDE_DIR`` + the Vulkan include directory +``Vulkan_LIBRARY`` + the path to the Vulkan library +``Vulkan_GLSLC_EXECUTABLE`` + the path to the GLSL SPIR-V compiler +``Vulkan_GLSLANG_VALIDATOR_EXECUTABLE`` + the path to the glslangValidator tool +``Vulkan_glslang_LIBRARY`` + .. versionadded:: 3.24 + + Path to the glslang library. +``Vulkan_shaderc_combined_LIBRARY`` + .. versionadded:: 3.24 + + Path to the shaderc_combined library. +``Vulkan_SPIRV-Tools_LIBRARY`` + .. versionadded:: 3.24 + + Path to the SPIRV-Tools library. +``Vulkan_MoltenVK_LIBRARY`` + .. versionadded:: 3.24 + + Path to the MoltenVK library. + +``Vulkan_volk_LIBRARY`` + .. versionadded:: 3.25 + + Path to the volk library. + +``Vulkan_dxc_LIBRARY`` + .. versionadded:: 3.25 + + Path to the DirectX shader compiler library. + +``Vulkan_dxc_EXECUTABLE`` + .. versionadded:: 3.25 + + Path to the DirectX shader compiler CLI tool. + +Hints +^^^^^ + +.. versionadded:: 3.18 + +The ``VULKAN_SDK`` environment variable optionally specifies the +location of the Vulkan SDK root directory for the given +architecture. It is typically set by sourcing the toplevel +``setup-env.sh`` script of the Vulkan SDK directory into the shell +environment. + +#]=======================================================================] + +cmake_policy(PUSH) +cmake_policy(SET CMP0057 NEW) + +# Provide compatibility with a common invalid component request that +# was silently ignored prior to CMake 3.24. +if("FATAL_ERROR" IN_LIST Vulkan_FIND_COMPONENTS) + message(AUTHOR_WARNING + "Ignoring unknown component 'FATAL_ERROR'.\n" + "The find_package() command documents no such argument." + ) + list(REMOVE_ITEM Vulkan_FIND_COMPONENTS "FATAL_ERROR") +endif() + +if(IOS) + get_filename_component(Vulkan_Target_SDK "$ENV{VULKAN_SDK}/.." REALPATH) + list(APPEND CMAKE_FRAMEWORK_PATH "${Vulkan_Target_SDK}/iOS/lib") + set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) + set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) + set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) +endif() + +# For backward compatibility as `FindVulkan` in previous CMake versions allow to retrieve `glslc` +# and `glslangValidator` without requesting the corresponding component. +if(NOT glslc IN_LIST Vulkan_FIND_COMPONENTS) + list(APPEND Vulkan_FIND_COMPONENTS glslc) +endif() +if(NOT glslangValidator IN_LIST Vulkan_FIND_COMPONENTS) + list(APPEND Vulkan_FIND_COMPONENTS glslangValidator) +endif() + +if(WIN32) + set(_Vulkan_library_name vulkan-1) + set(_Vulkan_hint_include_search_paths + "$ENV{VULKAN_SDK}/include" + ) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_Vulkan_hint_executable_search_paths + "$ENV{VULKAN_SDK}/bin" + ) + set(_Vulkan_hint_library_search_paths + "$ENV{VULKAN_SDK}/lib" + "$ENV{VULKAN_SDK}/bin" + ) + else() + set(_Vulkan_hint_executable_search_paths + "$ENV{VULKAN_SDK}/bin32" + "$ENV{VULKAN_SDK}/bin" + ) + set(_Vulkan_hint_library_search_paths + "$ENV{VULKAN_SDK}/lib32" + "$ENV{VULKAN_SDK}/bin32" + "$ENV{VULKAN_SDK}/lib" + "$ENV{VULKAN_SDK}/bin" + ) + endif() +else() + set(_Vulkan_library_name vulkan) + set(_Vulkan_hint_include_search_paths + "$ENV{VULKAN_SDK}/include" + ) + set(_Vulkan_hint_executable_search_paths + "$ENV{VULKAN_SDK}/bin" + ) + set(_Vulkan_hint_library_search_paths + "$ENV{VULKAN_SDK}/lib" + ) +endif() +if(APPLE AND DEFINED ENV{VULKAN_SDK}) + list(APPEND _Vulkan_hint_include_search_paths + "${Vulkan_Target_SDK}/macOS/include" + ) + if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + list(APPEND _Vulkan_hint_library_search_paths + "${Vulkan_Target_SDK}/iOS/lib" + ) + elseif(CMAKE_SYSTEM_NAME STREQUAL "tvOS") + list(APPEND _Vulkan_hint_library_search_paths + "${Vulkan_Target_SDK}/tvOS/lib" + ) + else() + list(APPEND _Vulkan_hint_library_search_paths + "${Vulkan_Target_SDK}}/lib" + ) + endif() +endif() + +find_path(Vulkan_INCLUDE_DIR + NAMES vulkan/vulkan.h + HINTS + ${_Vulkan_hint_include_search_paths} +) +message(STATUS "vulkan_include_dir ${Vulkan_INCLUDE_DIR} search paths ${_Vulkan_hint_include_search_paths}") +mark_as_advanced(Vulkan_INCLUDE_DIR) + +find_library(Vulkan_LIBRARY + NAMES ${_Vulkan_library_name} + HINTS + ${_Vulkan_hint_library_search_paths} +) +message(STATUS "vulkan_library ${Vulkan_LIBRARY} search paths ${_Vulkan_hint_library_search_paths}") +mark_as_advanced(Vulkan_LIBRARY) + +find_library(Vulkan_Layer_API_DUMP + NAMES VkLayer_api_dump + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(Vulkan_Layer_API_DUMP) + +find_library(Vulkan_Layer_SHADER_OBJECT + NAMES VkLayer_khronos_shader_object + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(VkLayer_khronos_shader_object) + +find_library(Vulkan_Layer_SYNC2 + NAMES VkLayer_khronos_synchronization2 + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(Vulkan_Layer_SYNC2) + +find_library(Vulkan_Layer_VALIDATION + NAMES VkLayer_khronos_validation + HINTS + ${_Vulkan_hint_library_search_paths} +) +mark_as_advanced(Vulkan_Layer_VALIDATION) + +if(glslc IN_LIST Vulkan_FIND_COMPONENTS) + find_program(Vulkan_GLSLC_EXECUTABLE + NAMES glslc + HINTS + ${_Vulkan_hint_executable_search_paths} + ) + mark_as_advanced(Vulkan_GLSLC_EXECUTABLE) +endif() +if(glslangValidator IN_LIST Vulkan_FIND_COMPONENTS) + find_program(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE + NAMES glslangValidator + HINTS + ${_Vulkan_hint_executable_search_paths} + ) + mark_as_advanced(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE) +endif() +if(glslang IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_glslang-spirv_LIBRARY + NAMES SPIRV + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-spirv_LIBRARY) + + find_library(Vulkan_glslang-spirv_DEBUG_LIBRARY + NAMES SPIRVd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-spirv_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-oglcompiler_LIBRARY + NAMES OGLCompiler + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-oglcompiler_LIBRARY) + + find_library(Vulkan_glslang-oglcompiler_DEBUG_LIBRARY + NAMES OGLCompilerd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-oglcompiler_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-osdependent_LIBRARY + NAMES OSDependent + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-osdependent_LIBRARY) + + find_library(Vulkan_glslang-osdependent_DEBUG_LIBRARY + NAMES OSDependentd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-osdependent_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-machineindependent_LIBRARY + NAMES MachineIndependent + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-machineindependent_LIBRARY) + + find_library(Vulkan_glslang-machineindependent_DEBUG_LIBRARY + NAMES MachineIndependentd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-machineindependent_DEBUG_LIBRARY) + + find_library(Vulkan_glslang-genericcodegen_LIBRARY + NAMES GenericCodeGen + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-genericcodegen_LIBRARY) + + find_library(Vulkan_glslang-genericcodegen_DEBUG_LIBRARY + NAMES GenericCodeGend + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang-genericcodegen_DEBUG_LIBRARY) + + find_library(Vulkan_glslang_LIBRARY + NAMES glslang + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang_LIBRARY) + + find_library(Vulkan_glslang_DEBUG_LIBRARY + NAMES glslangd + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_glslang_DEBUG_LIBRARY) +endif() +if(shaderc_combined IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_shaderc_combined_LIBRARY + NAMES shaderc_combined + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_shaderc_combined_LIBRARY) + + find_library(Vulkan_shaderc_combined_DEBUG_LIBRARY + NAMES shaderc_combinedd + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_shaderc_combined_DEBUG_LIBRARY) +endif() +if(SPIRV-Tools IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_SPIRV-Tools_LIBRARY + NAMES SPIRV-Tools + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_SPIRV-Tools_LIBRARY) + + find_library(Vulkan_SPIRV-Tools_DEBUG_LIBRARY + NAMES SPIRV-Toolsd + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_SPIRV-Tools_DEBUG_LIBRARY) +endif() +if(MoltenVK IN_LIST Vulkan_FIND_COMPONENTS) + # CMake has a bug in 3.28 that doesn't handle xcframeworks. Do it by hand for now. + if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + if(CMAKE_VERSION VERSION_LESS 3.29) + set( _Vulkan_hint_library_search_paths ${Vulkan_Target_SDK}/ios/lib/MoltenVK.xcframework/ios-arm64) + else () + set( _Vulkan_hint_library_search_paths ${Vulkan_Target_SDK}/ios/lib/) + endif () + endif () + find_library(Vulkan_MoltenVK_LIBRARY + NAMES MoltenVK + HINTS + ${_Vulkan_hint_library_search_paths} + ) + mark_as_advanced(Vulkan_MoltenVK_LIBRARY) + + find_path(Vulkan_MoltenVK_INCLUDE_DIR + NAMES MoltenVK/mvk_vulkan.h + HINTS + ${_Vulkan_hint_include_search_paths} + ) + mark_as_advanced(Vulkan_MoltenVK_INCLUDE_DIR) +endif() +if(volk IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_volk_LIBRARY + NAMES volk + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_Volk_LIBRARY) +endif() + +if (dxc IN_LIST Vulkan_FIND_COMPONENTS) + find_library(Vulkan_dxc_LIBRARY + NAMES dxcompiler + HINTS + ${_Vulkan_hint_library_search_paths}) + mark_as_advanced(Vulkan_dxc_LIBRARY) + + find_program(Vulkan_dxc_EXECUTABLE + NAMES dxc + HINTS + ${_Vulkan_hint_executable_search_paths}) + mark_as_advanced(Vulkan_dxc_EXECUTABLE) +endif() + +if(Vulkan_GLSLC_EXECUTABLE) + set(Vulkan_glslc_FOUND TRUE) +else() + set(Vulkan_glslc_FOUND FALSE) +endif() + +if(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE) + set(Vulkan_glslangValidator_FOUND TRUE) +else() + set(Vulkan_glslangValidator_FOUND FALSE) +endif() + +if (Vulkan_dxc_EXECUTABLE) + set(Vulkan_dxc_exe_FOUND TRUE) +else() + set(Vulkan_dxc_exe_FOUND FALSE) +endif() + +function(_Vulkan_set_library_component_found component) + cmake_parse_arguments(PARSE_ARGV 1 _ARG + "NO_WARNING" + "" + "DEPENDENT_COMPONENTS") + + set(all_dependent_component_found TRUE) + foreach(dependent_component IN LISTS _ARG_DEPENDENT_COMPONENTS) + if(NOT Vulkan_${dependent_component}_FOUND) + set(all_dependent_component_found FALSE) + break() + endif() + endforeach() + + if(all_dependent_component_found AND (Vulkan_${component}_LIBRARY OR Vulkan_${component}_DEBUG_LIBRARY)) + set(Vulkan_${component}_FOUND TRUE PARENT_SCOPE) + + # For Windows Vulkan SDK, third party tools binaries are provided with different MSVC ABI: + # - Release binaries uses a runtime library + # - Debug binaries uses a debug runtime library + # This lead to incompatibilities in linking for some configuration types due to CMake-default or project-configured selected MSVC ABI. + if(WIN32 AND NOT _ARG_NO_WARNING) + if(NOT Vulkan_${component}_LIBRARY) + message(WARNING + "Library ${component} for Release configuration is missing, imported target Vulkan::${component} may not be able to link when targeting this build configuration due to incompatible MSVC ABI.") + endif() + if(NOT Vulkan_${component}_DEBUG_LIBRARY) + message(WARNING + "Library ${component} for Debug configuration is missing, imported target Vulkan::${component} may not be able to link when targeting this build configuration due to incompatible MSVC ABI. Consider re-installing the Vulkan SDK and request debug libraries to fix this warning.") + endif() + endif() + else() + set(Vulkan_${component}_FOUND FALSE PARENT_SCOPE) + endif() +endfunction() + +_Vulkan_set_library_component_found(glslang-spirv NO_WARNING) +_Vulkan_set_library_component_found(glslang-oglcompiler NO_WARNING) +_Vulkan_set_library_component_found(glslang-osdependent NO_WARNING) +_Vulkan_set_library_component_found(glslang-machineindependent NO_WARNING) +_Vulkan_set_library_component_found(glslang-genericcodegen NO_WARNING) +_Vulkan_set_library_component_found(glslang + DEPENDENT_COMPONENTS + glslang-spirv + glslang-oglcompiler + glslang-osdependent + glslang-machineindependent + glslang-genericcodegen) +_Vulkan_set_library_component_found(shaderc_combined) +_Vulkan_set_library_component_found(SPIRV-Tools) +_Vulkan_set_library_component_found(volk) +_Vulkan_set_library_component_found(dxc) + +if(Vulkan_MoltenVK_INCLUDE_DIR AND Vulkan_MoltenVK_LIBRARY) + set(Vulkan_MoltenVK_FOUND TRUE) +else() + set(Vulkan_MoltenVK_FOUND FALSE) +endif() + +set(Vulkan_LIBRARIES ${Vulkan_LIBRARY}) +set(Vulkan_INCLUDE_DIRS ${Vulkan_INCLUDE_DIR}) + +# detect version e.g 1.2.189 +set(Vulkan_VERSION "") +if(Vulkan_INCLUDE_DIR) + set(VULKAN_CORE_H ${Vulkan_INCLUDE_DIR}/vulkan/vulkan_core.h) + if(EXISTS ${VULKAN_CORE_H}) + file(STRINGS ${VULKAN_CORE_H} VulkanHeaderVersionLine REGEX "^#define VK_HEADER_VERSION ") + string(REGEX MATCHALL "[0-9]+" VulkanHeaderVersion "${VulkanHeaderVersionLine}") + file(STRINGS ${VULKAN_CORE_H} VulkanHeaderVersionLine2 REGEX "^#define VK_HEADER_VERSION_COMPLETE ") + string(REGEX MATCHALL "[0-9]+" VulkanHeaderVersion2 "${VulkanHeaderVersionLine2}") + list(LENGTH VulkanHeaderVersion2 _len) + # versions >= 1.2.175 have an additional numbers in front of e.g. '0, 1, 2' instead of '1, 2' + if(_len EQUAL 3) + list(REMOVE_AT VulkanHeaderVersion2 0) + endif() + list(APPEND VulkanHeaderVersion2 ${VulkanHeaderVersion}) + list(JOIN VulkanHeaderVersion2 "." Vulkan_VERSION) + endif() +endif() + +if(Vulkan_MoltenVK_FOUND) + set(Vulkan_MoltenVK_VERSION "") + if(Vulkan_MoltenVK_INCLUDE_DIR) + set(VK_MVK_MOLTENVK_H ${Vulkan_MoltenVK_INCLUDE_DIR}/MoltenVK/vk_mvk_moltenvk.h) + if(EXISTS ${VK_MVK_MOLTENVK_H}) + file(STRINGS ${VK_MVK_MOLTENVK_H} _Vulkan_MoltenVK_VERSION_MAJOR REGEX "^#define MVK_VERSION_MAJOR ") + string(REGEX MATCHALL "[0-9]+" _Vulkan_MoltenVK_VERSION_MAJOR "${_Vulkan_MoltenVK_VERSION_MAJOR}") + file(STRINGS ${VK_MVK_MOLTENVK_H} _Vulkan_MoltenVK_VERSION_MINOR REGEX "^#define MVK_VERSION_MINOR ") + string(REGEX MATCHALL "[0-9]+" _Vulkan_MoltenVK_VERSION_MINOR "${_Vulkan_MoltenVK_VERSION_MINOR}") + file(STRINGS ${VK_MVK_MOLTENVK_H} _Vulkan_MoltenVK_VERSION_PATCH REGEX "^#define MVK_VERSION_PATCH ") + string(REGEX MATCHALL "[0-9]+" _Vulkan_MoltenVK_VERSION_PATCH "${_Vulkan_MoltenVK_VERSION_PATCH}") + set(Vulkan_MoltenVK_VERSION "${_Vulkan_MoltenVK_VERSION_MAJOR}.${_Vulkan_MoltenVK_VERSION_MINOR}.${_Vulkan_MoltenVK_VERSION_PATCH}") + unset(_Vulkan_MoltenVK_VERSION_MAJOR) + unset(_Vulkan_MoltenVK_VERSION_MINOR) + unset(_Vulkan_MoltenVK_VERSION_PATCH) + endif() + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Vulkan + REQUIRED_VARS + Vulkan_LIBRARY + Vulkan_INCLUDE_DIR + VERSION_VAR + Vulkan_VERSION + HANDLE_COMPONENTS +) + +if(Vulkan_FOUND AND NOT TARGET Vulkan::Vulkan) + add_library(Vulkan::Vulkan UNKNOWN IMPORTED) + set_target_properties(Vulkan::Vulkan PROPERTIES + IMPORTED_LOCATION "${Vulkan_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") +endif() + +if(Vulkan_FOUND AND NOT TARGET Vulkan::Headers) + add_library(Vulkan::Headers INTERFACE IMPORTED) + set_target_properties(Vulkan::Headers PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") +endif() + +if(Vulkan_FOUND AND Vulkan_GLSLC_EXECUTABLE AND NOT TARGET Vulkan::glslc) + add_executable(Vulkan::glslc IMPORTED) + set_property(TARGET Vulkan::glslc PROPERTY IMPORTED_LOCATION "${Vulkan_GLSLC_EXECUTABLE}") +endif() + +if(Vulkan_FOUND AND Vulkan_GLSLANG_VALIDATOR_EXECUTABLE AND NOT TARGET Vulkan::glslangValidator) + add_executable(Vulkan::glslangValidator IMPORTED) + set_property(TARGET Vulkan::glslangValidator PROPERTY IMPORTED_LOCATION "${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}") +endif() + +if(Vulkan_FOUND) + if((Vulkan_glslang-spirv_LIBRARY OR Vulkan_glslang-spirv_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-spirv) + add_library(Vulkan::glslang-spirv STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-spirv + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-spirv_LIBRARY) + set_property(TARGET Vulkan::glslang-spirv APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-spirv + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-spirv_LIBRARY}") + endif() + if(Vulkan_glslang-spirv_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-spirv APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-spirv + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-spirv_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-oglcompiler_LIBRARY OR Vulkan_glslang-oglcompiler_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-oglcompiler) + add_library(Vulkan::glslang-oglcompiler STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-oglcompiler + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-oglcompiler_LIBRARY) + set_property(TARGET Vulkan::glslang-oglcompiler APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-oglcompiler + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-oglcompiler_LIBRARY}") + endif() + if(Vulkan_glslang-oglcompiler_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-oglcompiler APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-oglcompiler + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-oglcompiler_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-osdependent_LIBRARY OR Vulkan_glslang-osdependent_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-osdependent) + add_library(Vulkan::glslang-osdependent STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-osdependent + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-osdependent_LIBRARY) + set_property(TARGET Vulkan::glslang-osdependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-osdependent + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-osdependent_LIBRARY}") + endif() + if(Vulkan_glslang-osdependent_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-osdependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-osdependent + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-osdependent_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-machineindependent_LIBRARY OR Vulkan_glslang-machineindependent_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-machineindependent) + add_library(Vulkan::glslang-machineindependent STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-machineindependent + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-machineindependent_LIBRARY) + set_property(TARGET Vulkan::glslang-machineindependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-machineindependent + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-machineindependent_LIBRARY}") + endif() + if(Vulkan_glslang-machineindependent_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-machineindependent APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-machineindependent + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-machineindependent_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang-genericcodegen_LIBRARY OR Vulkan_glslang-genericcodegen_DEBUG_LIBRARY) AND NOT TARGET Vulkan::glslang-genericcodegen) + add_library(Vulkan::glslang-genericcodegen STATIC IMPORTED) + set_property(TARGET Vulkan::glslang-genericcodegen + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang-genericcodegen_LIBRARY) + set_property(TARGET Vulkan::glslang-genericcodegen APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang-genericcodegen + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang-genericcodegen_LIBRARY}") + endif() + if(Vulkan_glslang-genericcodegen_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang-genericcodegen APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang-genericcodegen + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang-genericcodegen_DEBUG_LIBRARY}") + endif() + endif() + + if((Vulkan_glslang_LIBRARY OR Vulkan_glslang_DEBUG_LIBRARY) + AND TARGET Vulkan::glslang-spirv + AND TARGET Vulkan::glslang-oglcompiler + AND TARGET Vulkan::glslang-osdependent + AND TARGET Vulkan::glslang-machineindependent + AND TARGET Vulkan::glslang-genericcodegen + AND NOT TARGET Vulkan::glslang) + add_library(Vulkan::glslang STATIC IMPORTED) + set_property(TARGET Vulkan::glslang + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_glslang_LIBRARY) + set_property(TARGET Vulkan::glslang APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::glslang + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_glslang_LIBRARY}") + endif() + if(Vulkan_glslang_DEBUG_LIBRARY) + set_property(TARGET Vulkan::glslang APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::glslang + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_glslang_DEBUG_LIBRARY}") + endif() + target_link_libraries(Vulkan::glslang + INTERFACE + Vulkan::glslang-spirv + Vulkan::glslang-oglcompiler + Vulkan::glslang-osdependent + Vulkan::glslang-machineindependent + Vulkan::glslang-genericcodegen + ) + endif() + + if((Vulkan_shaderc_combined_LIBRARY OR Vulkan_shaderc_combined_DEBUG_LIBRARY) AND NOT TARGET Vulkan::shaderc_combined) + add_library(Vulkan::shaderc_combined STATIC IMPORTED) + set_property(TARGET Vulkan::shaderc_combined + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_shaderc_combined_LIBRARY) + set_property(TARGET Vulkan::shaderc_combined APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::shaderc_combined + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_shaderc_combined_LIBRARY}") + endif() + if(Vulkan_shaderc_combined_DEBUG_LIBRARY) + set_property(TARGET Vulkan::shaderc_combined APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::shaderc_combined + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_shaderc_combined_DEBUG_LIBRARY}") + endif() + + if(UNIX) + find_package(Threads REQUIRED) + target_link_libraries(Vulkan::shaderc_combined + INTERFACE + Threads::Threads) + endif() + endif() + + if((Vulkan_SPIRV-Tools_LIBRARY OR Vulkan_SPIRV-Tools_DEBUG_LIBRARY) AND NOT TARGET Vulkan::SPIRV-Tools) + add_library(Vulkan::SPIRV-Tools STATIC IMPORTED) + set_property(TARGET Vulkan::SPIRV-Tools + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + if(Vulkan_SPIRV-Tools_LIBRARY) + set_property(TARGET Vulkan::SPIRV-Tools APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::SPIRV-Tools + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_SPIRV-Tools_LIBRARY}") + endif() + if(Vulkan_SPIRV-Tools_DEBUG_LIBRARY) + set_property(TARGET Vulkan::SPIRV-Tools APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Debug) + set_property(TARGET Vulkan::SPIRV-Tools + PROPERTY + IMPORTED_LOCATION_DEBUG "${Vulkan_SPIRV-Tools_DEBUG_LIBRARY}") + endif() + endif() + + if(Vulkan_volk_LIBRARY AND NOT TARGET Vulkan::volk) + add_library(Vulkan::volk STATIC IMPORTED) + set_property(TARGET Vulkan::volk + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + set_property(TARGET Vulkan::volk APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::volk APPEND + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_volk_LIBRARY}") + + if (NOT WIN32) + set_property(TARGET Vulkan::volk APPEND + PROPERTY + IMPORTED_LINK_INTERFACE_LIBRARIES dl) + endif() + endif() + + if (Vulkan_dxc_LIBRARY AND NOT TARGET Vulkan::dxc_lib) + add_library(Vulkan::dxc_lib STATIC IMPORTED) + set_property(TARGET Vulkan::dxc_lib + PROPERTY + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_INCLUDE_DIRS}") + set_property(TARGET Vulkan::dxc_lib APPEND + PROPERTY + IMPORTED_CONFIGURATIONS Release) + set_property(TARGET Vulkan::dxc_lib APPEND + PROPERTY + IMPORTED_LOCATION_RELEASE "${Vulkan_dxc_LIBRARY}") + endif() + + if(Vulkan_dxc_EXECUTABLE AND NOT TARGET Vulkan::dxc_exe) + add_executable(Vulkan::dxc_exe IMPORTED) + set_property(TARGET Vulkan::dxc_exe PROPERTY IMPORTED_LOCATION "${Vulkan_dxc_EXECUTABLE}") + endif() + +endif() + +if(Vulkan_MoltenVK_FOUND) + if(Vulkan_MoltenVK_LIBRARY AND NOT TARGET Vulkan::MoltenVK) + add_library(Vulkan::MoltenVK SHARED IMPORTED) + set_target_properties(Vulkan::MoltenVK + PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${Vulkan_MoltenVK_INCLUDE_DIR}" + IMPORTED_LOCATION "${Vulkan_MoltenVK_LIBRARY}" + ) + endif() +endif() + +unset(_Vulkan_library_name) +unset(_Vulkan_hint_include_search_paths) +unset(_Vulkan_hint_executable_search_paths) +unset(_Vulkan_hint_library_search_paths) + +cmake_policy(POP) \ No newline at end of file diff --git a/CMake/FindVulkanHpp.cmake b/CMake/FindVulkanHpp.cmake new file mode 100644 index 0000000..2c0e23a --- /dev/null +++ b/CMake/FindVulkanHpp.cmake @@ -0,0 +1,426 @@ +# FindVulkanHpp.cmake +# +# Finds or downloads the Vulkan-Hpp headers and Vulkan Profiles headers +# +# This will define the following variables +# +# VulkanHpp_FOUND +# VulkanHpp_INCLUDE_DIRS +# +# and the following imported targets +# +# VulkanHpp::VulkanHpp +# + +# Try to find the package using standard find_path +find_path(VulkanHpp_INCLUDE_DIR + NAMES vulkan/vulkan.hpp + PATHS + ${Vulkan_INCLUDE_DIR} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include +) + +# Also try to find vulkan.cppm +find_path(VulkanHpp_CPPM_DIR + NAMES vulkan/vulkan.cppm + PATHS + ${Vulkan_INCLUDE_DIR} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include +) + +# Try to find vulkan_profiles.hpp +find_path(VulkanProfiles_INCLUDE_DIR + NAMES vulkan/vulkan_profiles.hpp + PATHS + ${Vulkan_INCLUDE_DIR} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include +) + +# Function to extract Vulkan version from vulkan_core.h +function(extract_vulkan_version VULKAN_CORE_H_PATH OUTPUT_VERSION_TAG) + # Extract the version information from vulkan_core.h + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_VERSION_MAJOR_LINE REGEX "^#define VK_VERSION_MAJOR") + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_VERSION_MINOR_LINE REGEX "^#define VK_VERSION_MINOR") + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_HEADER_VERSION_LINE REGEX "^#define VK_HEADER_VERSION") + + set(VERSION_TAG "v1.3.275") # Default fallback + + if(VULKAN_VERSION_MAJOR_LINE AND VULKAN_VERSION_MINOR_LINE AND VULKAN_HEADER_VERSION_LINE) + string(REGEX REPLACE "^#define VK_VERSION_MAJOR[ \t]+([0-9]+).*$" "\\1" VULKAN_VERSION_MAJOR "${VULKAN_VERSION_MAJOR_LINE}") + string(REGEX REPLACE "^#define VK_VERSION_MINOR[ \t]+([0-9]+).*$" "\\1" VULKAN_VERSION_MINOR "${VULKAN_VERSION_MINOR_LINE}") + string(REGEX REPLACE "^#define VK_HEADER_VERSION[ \t]+([0-9]+).*$" "\\1" VULKAN_HEADER_VERSION "${VULKAN_HEADER_VERSION_LINE}") + + # Construct the version tag + set(VERSION_TAG "v${VULKAN_VERSION_MAJOR}.${VULKAN_VERSION_MINOR}.${VULKAN_HEADER_VERSION}") + else() + # Alternative approach: look for VK_HEADER_VERSION_COMPLETE + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_HEADER_VERSION_COMPLETE_LINE REGEX "^#define VK_HEADER_VERSION_COMPLETE") + file(STRINGS ${VULKAN_CORE_H_PATH} VULKAN_HEADER_VERSION_LINE REGEX "^#define VK_HEADER_VERSION") + + if(VULKAN_HEADER_VERSION_COMPLETE_LINE AND VULKAN_HEADER_VERSION_LINE) + # Extract the header version + string(REGEX REPLACE "^#define VK_HEADER_VERSION[ \t]+([0-9]+).*$" "\\1" VULKAN_HEADER_VERSION "${VULKAN_HEADER_VERSION_LINE}") + + # Check if the complete version line contains the major and minor versions + if(VULKAN_HEADER_VERSION_COMPLETE_LINE MATCHES "VK_MAKE_API_VERSION\\(.*,[ \t]*([0-9]+),[ \t]*([0-9]+),[ \t]*VK_HEADER_VERSION\\)") + set(VULKAN_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(VULKAN_VERSION_MINOR "${CMAKE_MATCH_2}") + set(VERSION_TAG "v${VULKAN_VERSION_MAJOR}.${VULKAN_VERSION_MINOR}.${VULKAN_HEADER_VERSION}") + endif() + endif() + endif() + + # Return the version tag + set(${OUTPUT_VERSION_TAG} ${VERSION_TAG} PARENT_SCOPE) +endfunction() + +# Determine the Vulkan version to use for Vulkan-Hpp and Vulkan-Profiles +set(VULKAN_VERSION_TAG "v1.3.275") # Default version + +# Try to detect the Vulkan version +set(VULKAN_CORE_H "") + +# If we're building for Android, try to detect the NDK's Vulkan version +if(DEFINED ANDROID_NDK) + # Find the vulkan_core.h file in the NDK + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + ${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/vulkan + ${ANDROID_NDK}/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/vulkan + ${ANDROID_NDK}/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include/vulkan + ${ANDROID_NDK}/toolchains/llvm/prebuilt/windows/sysroot/usr/include/vulkan + NO_DEFAULT_PATH + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected NDK Vulkan version: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in NDK, using default version: ${VULKAN_VERSION_TAG}") + endif() +# For desktop builds, try to detect the Vulkan SDK version +elseif(DEFINED ENV{VULKAN_SDK}) + # Find the vulkan_core.h file in the Vulkan SDK + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + $ENV{VULKAN_SDK}/include/vulkan + NO_DEFAULT_PATH + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected Vulkan SDK version: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in Vulkan SDK, using default version: ${VULKAN_VERSION_TAG}") + endif() +# If Vulkan package was already found, try to use its include directory +elseif(DEFINED Vulkan_INCLUDE_DIR) + # Find the vulkan_core.h file in the Vulkan include directory + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + ${Vulkan_INCLUDE_DIR}/vulkan + NO_DEFAULT_PATH + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected Vulkan version from include directory: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in Vulkan include directory, using default version: ${VULKAN_VERSION_TAG}") + endif() +else() + # Try to find vulkan_core.h in system paths + find_file(VULKAN_CORE_H vulkan_core.h + PATHS + /usr/include/vulkan + /usr/local/include/vulkan + ) + + if(VULKAN_CORE_H) + extract_vulkan_version(${VULKAN_CORE_H} VULKAN_VERSION_TAG) + message(STATUS "Detected system Vulkan version: ${VULKAN_VERSION_TAG}") + else() + message(STATUS "Could not find vulkan_core.h in system paths, using default version: ${VULKAN_VERSION_TAG}") + endif() +endif() + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT VulkanHpp_INCLUDE_DIR OR NOT VulkanHpp_CPPM_DIR) + # If not found, use FetchContent to download + include(FetchContent) + + message(STATUS "Vulkan-Hpp not found, fetching from GitHub with version ${VULKAN_VERSION_TAG}...") + FetchContent_Declare( + VulkanHpp + GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Hpp.git + GIT_TAG ${VULKAN_VERSION_TAG} # Use the detected or default version + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Make sure FetchContent is available + include(FetchContent) + + # Populate the content + FetchContent_GetProperties(VulkanHpp SOURCE_DIR VulkanHpp_SOURCE_DIR) + if(NOT VulkanHpp_POPULATED) + FetchContent_Populate(VulkanHpp) + # Get the source directory after populating + FetchContent_GetProperties(VulkanHpp SOURCE_DIR VulkanHpp_SOURCE_DIR) + endif() + + # Set the include directory to the source directory + set(VulkanHpp_INCLUDE_DIR ${VulkanHpp_SOURCE_DIR}) + message(STATUS "VulkanHpp_SOURCE_DIR: ${VulkanHpp_SOURCE_DIR}") + message(STATUS "VulkanHpp_INCLUDE_DIR: ${VulkanHpp_INCLUDE_DIR}") + + # Check if vulkan.cppm exists in the downloaded repository + if(EXISTS "${VulkanHpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_SOURCE_DIR}) + else() + # If vulkan.cppm doesn't exist, we need to create it + set(VulkanHpp_CPPM_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanHpp) + file(MAKE_DIRECTORY ${VulkanHpp_CPPM_DIR}/vulkan) + + # Create vulkan.cppm file + file(WRITE "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" +"// Auto-generated vulkan.cppm file +module; +#include +export module vulkan; +export namespace vk { + using namespace VULKAN_HPP_NAMESPACE; +} +") + endif() +endif() + +# If the Vulkan Profiles include directory wasn't found, use FetchContent to download +if(NOT VulkanProfiles_INCLUDE_DIR) + # If not found, use FetchContent to download + include(FetchContent) + + message(STATUS "Vulkan-Profiles not found, fetching from GitHub main branch...") + FetchContent_Declare( + VulkanProfiles + GIT_REPOSITORY https://github.com/KhronosGroup/Vulkan-Profiles.git + GIT_TAG main # Use main branch instead of a specific tag + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content + FetchContent_GetProperties(VulkanProfiles SOURCE_DIR VulkanProfiles_SOURCE_DIR) + if(NOT VulkanProfiles_POPULATED) + FetchContent_Populate(VulkanProfiles) + # Get the source directory after populating + FetchContent_GetProperties(VulkanProfiles SOURCE_DIR VulkanProfiles_SOURCE_DIR) + endif() + + # Create the include directory structure if it doesn't exist + set(VulkanProfiles_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanProfiles/include) + file(MAKE_DIRECTORY ${VulkanProfiles_INCLUDE_DIR}/vulkan) + + # Create a stub vulkan_profiles.hpp file if it doesn't exist + if(NOT EXISTS "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp") + file(WRITE "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp" +"// Auto-generated vulkan_profiles.hpp stub file +#pragma once +#include + +namespace vp { + // Stub implementation for Vulkan Profiles + struct ProfileDesc { + const char* name; + uint32_t specVersion; + }; + + inline bool GetProfileSupport(VkPhysicalDevice physicalDevice, const ProfileDesc* pProfile, VkBool32* pSupported) { + *pSupported = VK_TRUE; + return true; + } +} +") + endif() + + message(STATUS "VulkanProfiles_SOURCE_DIR: ${VulkanProfiles_SOURCE_DIR}") + message(STATUS "VulkanProfiles_INCLUDE_DIR: ${VulkanProfiles_INCLUDE_DIR}") +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(VulkanHpp + REQUIRED_VARS VulkanHpp_INCLUDE_DIR + FAIL_MESSAGE "Could NOT find VulkanHpp. Install it or set VulkanHpp_INCLUDE_DIR to the directory containing vulkan/vulkan.hpp" +) + +# Debug output +message(STATUS "VulkanHpp_FOUND: ${VulkanHpp_FOUND}") +message(STATUS "VULKANHPP_FOUND: ${VULKANHPP_FOUND}") + +if(VulkanHpp_FOUND) + set(VulkanHpp_INCLUDE_DIRS ${VulkanHpp_INCLUDE_DIR}) + + # Make sure VulkanHpp_CPPM_DIR is set + if(NOT DEFINED VulkanHpp_CPPM_DIR) + # Check if vulkan.cppm exists in the include directory + if(EXISTS "${VulkanHpp_INCLUDE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_INCLUDE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_INCLUDE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED VulkanHpp_SOURCE_DIR AND EXISTS "${VulkanHpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED vulkanhpp_SOURCE_DIR AND EXISTS "${vulkanhpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${vulkanhpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in vulkanhpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + else() + # If vulkan.cppm doesn't exist, we need to create it + set(VulkanHpp_CPPM_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanHpp) + file(MAKE_DIRECTORY ${VulkanHpp_CPPM_DIR}/vulkan) + message(STATUS "Creating vulkan.cppm in ${VulkanHpp_CPPM_DIR}") + + # Create vulkan.cppm file + file(WRITE "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" +"// Auto-generated vulkan.cppm file +module; +#include +export module vulkan; +export namespace vk { + using namespace VULKAN_HPP_NAMESPACE; +} +") + endif() + endif() + + message(STATUS "Final VulkanHpp_CPPM_DIR: ${VulkanHpp_CPPM_DIR}") + + # Add Vulkan Profiles include directory if found + if(VulkanProfiles_INCLUDE_DIR AND EXISTS "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp") + list(APPEND VulkanHpp_INCLUDE_DIRS ${VulkanProfiles_INCLUDE_DIR}) + message(STATUS "Added Vulkan Profiles include directory: ${VulkanProfiles_INCLUDE_DIR}") + endif() + + # Create an imported target + if(NOT TARGET VulkanHpp::VulkanHpp) + add_library(VulkanHpp::VulkanHpp INTERFACE IMPORTED) + set_target_properties(VulkanHpp::VulkanHpp PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${VulkanHpp_INCLUDE_DIRS}" + ) + endif() +elseif(DEFINED VulkanHpp_SOURCE_DIR OR DEFINED vulkanhpp_SOURCE_DIR) + # If find_package_handle_standard_args failed but we have a VulkanHpp source directory from FetchContent + # Create an imported target + if(NOT TARGET VulkanHpp::VulkanHpp) + add_library(VulkanHpp::VulkanHpp INTERFACE IMPORTED) + + # Determine the source directory + if(DEFINED VulkanHpp_SOURCE_DIR) + set(_vulkanhpp_source_dir ${VulkanHpp_SOURCE_DIR}) + elseif(DEFINED vulkanhpp_SOURCE_DIR) + set(_vulkanhpp_source_dir ${vulkanhpp_SOURCE_DIR}) + endif() + + message(STATUS "Using fallback VulkanHpp source directory: ${_vulkanhpp_source_dir}") + + set_target_properties(VulkanHpp::VulkanHpp PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_vulkanhpp_source_dir}" + ) + endif() + + # Set variables to indicate that VulkanHpp was found + set(VulkanHpp_FOUND TRUE) + set(VULKANHPP_FOUND TRUE) + + # Set include directories + if(DEFINED _vulkanhpp_source_dir) + set(VulkanHpp_INCLUDE_DIR ${_vulkanhpp_source_dir}) + elseif(DEFINED VulkanHpp_SOURCE_DIR) + set(VulkanHpp_INCLUDE_DIR ${VulkanHpp_SOURCE_DIR}) + elseif(DEFINED vulkanhpp_SOURCE_DIR) + set(VulkanHpp_INCLUDE_DIR ${vulkanhpp_SOURCE_DIR}) + endif() + set(VulkanHpp_INCLUDE_DIRS ${VulkanHpp_INCLUDE_DIR}) + + # Add Vulkan Profiles include directory if found + if(VulkanProfiles_INCLUDE_DIR AND EXISTS "${VulkanProfiles_INCLUDE_DIR}/vulkan/vulkan_profiles.hpp") + list(APPEND VulkanHpp_INCLUDE_DIRS ${VulkanProfiles_INCLUDE_DIR}) + message(STATUS "Added Vulkan Profiles include directory to fallback: ${VulkanProfiles_INCLUDE_DIR}") + endif() + + # Make sure VulkanHpp_CPPM_DIR is set + if(NOT DEFINED VulkanHpp_CPPM_DIR) + # Check if vulkan.cppm exists in the downloaded repository + if(DEFINED VulkanHpp_INCLUDE_DIR AND EXISTS "${VulkanHpp_INCLUDE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_INCLUDE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_INCLUDE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED _vulkanhpp_source_dir AND EXISTS "${_vulkanhpp_source_dir}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${_vulkanhpp_source_dir}) + message(STATUS "Found vulkan.cppm in _vulkanhpp_source_dir: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED VulkanHpp_SOURCE_DIR AND EXISTS "${VulkanHpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${VulkanHpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in VulkanHpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + elseif(DEFINED vulkanhpp_SOURCE_DIR AND EXISTS "${vulkanhpp_SOURCE_DIR}/vulkan/vulkan.cppm") + set(VulkanHpp_CPPM_DIR ${vulkanhpp_SOURCE_DIR}) + message(STATUS "Found vulkan.cppm in vulkanhpp_SOURCE_DIR: ${VulkanHpp_CPPM_DIR}") + else() + # If vulkan.cppm doesn't exist, we need to create it + set(VulkanHpp_CPPM_DIR ${CMAKE_CURRENT_BINARY_DIR}/VulkanHpp) + file(MAKE_DIRECTORY ${VulkanHpp_CPPM_DIR}/vulkan) + message(STATUS "Creating vulkan.cppm in ${VulkanHpp_CPPM_DIR}") + + # Create vulkan.cppm file + file(WRITE "${VulkanHpp_CPPM_DIR}/vulkan/vulkan.cppm" +"// Auto-generated vulkan.cppm file +module; +#include +export module vulkan; +export namespace vk { + using namespace VULKAN_HPP_NAMESPACE; +} +") + endif() + endif() + + message(STATUS "Final VulkanHpp_CPPM_DIR: ${VulkanHpp_CPPM_DIR}") +endif() + +mark_as_advanced(VulkanHpp_INCLUDE_DIR VulkanHpp_CPPM_DIR) diff --git a/CMake/Findglm.cmake b/CMake/Findglm.cmake new file mode 100644 index 0000000..fdf113c --- /dev/null +++ b/CMake/Findglm.cmake @@ -0,0 +1,133 @@ +# Findglm.cmake +# +# Finds the GLM library +# +# This will define the following variables +# +# glm_FOUND +# glm_INCLUDE_DIRS +# +# and the following imported targets +# +# glm::glm +# + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_glm QUIET glm) +endif() + +# Find the include directory +find_path(glm_INCLUDE_DIR + NAMES glm/glm.hpp + PATHS + ${PC_glm_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES glm +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT glm_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "GLM not found, fetching from GitHub...") + FetchContent_Declare( + glm + GIT_REPOSITORY https://github.com/g-truc/glm.git + GIT_TAG 0.9.9.8 # Use a specific tag for stability + ) + + # Define a function to update the CMake minimum required version + function(update_glm_cmake_version) + # Get the source directory + FetchContent_GetProperties(glm SOURCE_DIR glm_SOURCE_DIR) + + # Update the minimum required CMake version + file(READ "${glm_SOURCE_DIR}/CMakeLists.txt" GLM_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.2" + "cmake_minimum_required(VERSION 3.5" + GLM_CMAKE_CONTENT "${GLM_CMAKE_CONTENT}") + file(WRITE "${glm_SOURCE_DIR}/CMakeLists.txt" "${GLM_CMAKE_CONTENT}") + endfunction() + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # First, declare and populate the content + FetchContent_GetProperties(glm) + if(NOT glm_POPULATED) + FetchContent_Populate(glm) + # Update the CMake version before making it available + update_glm_cmake_version() + endif() + + # Now make it available (this will process the CMakeLists.txt) + FetchContent_MakeAvailable(glm) + + # Get the include directory from the target + if(TARGET glm) + get_target_property(glm_INCLUDE_DIR glm INTERFACE_INCLUDE_DIRECTORIES) + if(NOT glm_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + set(glm_INCLUDE_DIR ${glm_SOURCE_DIR}) + endif() + else() + # GLM might not create a target, so use the source directory + set(glm_INCLUDE_DIR ${glm_SOURCE_DIR}) + endif() +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(glm + REQUIRED_VARS glm_INCLUDE_DIR +) + +if(glm_FOUND) + set(glm_INCLUDE_DIRS ${glm_INCLUDE_DIR}) + + # Create an imported target + if(NOT TARGET glm::glm) + add_library(glm::glm INTERFACE IMPORTED) + set_target_properties(glm::glm PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${glm_INCLUDE_DIRS}" + ) + endif() +elseif(TARGET glm) + # If find_package_handle_standard_args failed but we have a glm target from FetchContent + # Create an alias for the glm target + if(NOT TARGET glm::glm) + add_library(glm::glm ALIAS glm) + endif() + + # Set variables to indicate that glm was found + set(glm_FOUND TRUE) + set(GLM_FOUND TRUE) + + # Set include directories + get_target_property(glm_INCLUDE_DIR glm INTERFACE_INCLUDE_DIRECTORIES) + if(glm_INCLUDE_DIR) + set(glm_INCLUDE_DIRS ${glm_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + set(glm_INCLUDE_DIR ${glm_SOURCE_DIR}) + set(glm_INCLUDE_DIRS ${glm_INCLUDE_DIR}) + endif() +endif() + +mark_as_advanced(glm_INCLUDE_DIR) diff --git a/CMake/Findnlohmann_json.cmake b/CMake/Findnlohmann_json.cmake new file mode 100644 index 0000000..61dc66a --- /dev/null +++ b/CMake/Findnlohmann_json.cmake @@ -0,0 +1,154 @@ +# Findnlohmann_json.cmake +# +# Finds the nlohmann_json library +# +# This will define the following variables +# +# nlohmann_json_FOUND +# nlohmann_json_INCLUDE_DIRS +# +# and the following imported targets +# +# nlohmann_json::nlohmann_json +# + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_nlohmann_json QUIET nlohmann_json) +endif() + +# Find the include directory +find_path(nlohmann_json_INCLUDE_DIR + NAMES nlohmann/json.hpp json.hpp + PATHS + ${PC_nlohmann_json_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES nlohmann json +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT nlohmann_json_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "nlohmann_json not found, fetching from GitHub...") + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 # Use a specific tag for stability + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content but don't configure it yet + FetchContent_GetProperties(nlohmann_json) + if(NOT nlohmann_json_POPULATED) + FetchContent_Populate(nlohmann_json) + + if(ANDROID) + # Update the minimum required CMake version before including the CMakeLists.txt + file(READ "${nlohmann_json_SOURCE_DIR}/CMakeLists.txt" NLOHMANN_JSON_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.1" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.2" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.3" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.4" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.5" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.6" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.7" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.8" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.9" + "cmake_minimum_required(VERSION 3.10" + NLOHMANN_JSON_CMAKE_CONTENT "${NLOHMANN_JSON_CMAKE_CONTENT}") + file(WRITE "${nlohmann_json_SOURCE_DIR}/CMakeLists.txt" "${NLOHMANN_JSON_CMAKE_CONTENT}") + endif() + + # Now add the subdirectory manually + add_subdirectory(${nlohmann_json_SOURCE_DIR} ${nlohmann_json_BINARY_DIR}) + else() + # If already populated, just make it available + FetchContent_MakeAvailable(nlohmann_json) + endif() + + # Get the include directory from the target + if(TARGET nlohmann_json) + get_target_property(nlohmann_json_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) + if(NOT nlohmann_json_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + set(nlohmann_json_INCLUDE_DIR ${nlohmann_json_SOURCE_DIR}/include) + endif() + else() + # nlohmann_json might not create a target, so use the source directory + set(nlohmann_json_INCLUDE_DIR ${nlohmann_json_SOURCE_DIR}/include) + endif() +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(nlohmann_json + REQUIRED_VARS nlohmann_json_INCLUDE_DIR +) + +if(nlohmann_json_FOUND) + set(nlohmann_json_INCLUDE_DIRS ${nlohmann_json_INCLUDE_DIR}) + + # Create an imported target + if(NOT TARGET nlohmann_json::nlohmann_json) + add_library(nlohmann_json::nlohmann_json INTERFACE IMPORTED) + set_target_properties(nlohmann_json::nlohmann_json PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${nlohmann_json_INCLUDE_DIRS}" + ) + endif() +elseif(TARGET nlohmann_json) + # If find_package_handle_standard_args failed but we have a nlohmann_json target from FetchContent + # Create an alias for the nlohmann_json target + if(NOT TARGET nlohmann_json::nlohmann_json) + add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) + endif() + + # Set variables to indicate that nlohmann_json was found + set(nlohmann_json_FOUND TRUE) + set(NLOHMANN_JSON_FOUND TRUE) + + # Set include directories + get_target_property(nlohmann_json_INCLUDE_DIR nlohmann_json INTERFACE_INCLUDE_DIRECTORIES) + if(nlohmann_json_INCLUDE_DIR) + set(nlohmann_json_INCLUDE_DIRS ${nlohmann_json_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + set(nlohmann_json_INCLUDE_DIR ${nlohmann_json_SOURCE_DIR}/include) + set(nlohmann_json_INCLUDE_DIRS ${nlohmann_json_INCLUDE_DIR}) + endif() +endif() + +mark_as_advanced(nlohmann_json_INCLUDE_DIR) diff --git a/CMake/Findstb.cmake b/CMake/Findstb.cmake new file mode 100644 index 0000000..6ccf72f --- /dev/null +++ b/CMake/Findstb.cmake @@ -0,0 +1,86 @@ +# Findstb.cmake +# +# Finds the stb library (specifically stb_image.h) +# +# This will define the following variables +# +# stb_FOUND +# stb_INCLUDE_DIRS +# +# and the following imported targets +# +# stb::stb +# + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_stb QUIET stb) +endif() + +# Find the include directory +find_path(stb_INCLUDE_DIR + NAMES stb_image.h + PATHS + ${PC_stb_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES stb +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT stb_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "stb_image.h not found, fetching from GitHub...") + FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master # stb doesn't use version tags, so we use master + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content + FetchContent_GetProperties(stb) + if(NOT stb_POPULATED) + FetchContent_Populate(stb) + endif() + + # stb is a header-only library with no CMakeLists.txt, so we just need to set the include directory + set(stb_INCLUDE_DIR ${stb_SOURCE_DIR}) +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(stb + REQUIRED_VARS stb_INCLUDE_DIR +) + +if(stb_FOUND) + set(stb_INCLUDE_DIRS ${stb_INCLUDE_DIR}) + + # Create an imported target + if(NOT TARGET stb::stb) + add_library(stb::stb INTERFACE IMPORTED) + set_target_properties(stb::stb PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${stb_INCLUDE_DIRS}" + ) + endif() +endif() + +mark_as_advanced(stb_INCLUDE_DIR) \ No newline at end of file diff --git a/CMake/Findtinygltf.cmake b/CMake/Findtinygltf.cmake new file mode 100644 index 0000000..b241235 --- /dev/null +++ b/CMake/Findtinygltf.cmake @@ -0,0 +1,162 @@ +# Findtinygltf.cmake +# +# Finds the tinygltf library +# +# This will define the following variables +# +# tinygltf_FOUND +# tinygltf_INCLUDE_DIRS +# +# and the following imported targets +# +# tinygltf::tinygltf +# + +# First, try to find nlohmann_json +find_package(nlohmann_json QUIET) +if(NOT nlohmann_json_FOUND) + include(FetchContent) + message(STATUS "nlohmann_json not found, fetching v3.12.0 from GitHub...") + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 # Use a specific tag for stability + ) + FetchContent_MakeAvailable(nlohmann_json) +endif() + +# Try to find tinygltf using standard find_package +find_path(tinygltf_INCLUDE_DIR + NAMES tiny_gltf.h + PATHS + ${PC_tinygltf_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES tinygltf include +) + +# If not found, use FetchContent to download and build +if(NOT tinygltf_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "tinygltf not found, fetching from GitHub...") + FetchContent_Declare( + tinygltf + GIT_REPOSITORY https://github.com/syoyo/tinygltf.git + GIT_TAG v2.8.18 # Use a specific tag for stability + ) + + # Set policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content but don't configure it yet + FetchContent_GetProperties(tinygltf) + if(NOT tinygltf_POPULATED) + FetchContent_Populate(tinygltf) + + # Update the minimum required CMake version to avoid deprecation warning + file(READ "${tinygltf_SOURCE_DIR}/CMakeLists.txt" TINYGLTF_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.6)" + "cmake_minimum_required(VERSION 3.10)" + TINYGLTF_CMAKE_CONTENT "${TINYGLTF_CMAKE_CONTENT}") + file(WRITE "${tinygltf_SOURCE_DIR}/CMakeLists.txt" "${TINYGLTF_CMAKE_CONTENT}") + + # Create a symbolic link to make nlohmann/json.hpp available + if(EXISTS "${tinygltf_SOURCE_DIR}/json.hpp") + file(MAKE_DIRECTORY "${tinygltf_SOURCE_DIR}/nlohmann") + file(CREATE_LINK "${tinygltf_SOURCE_DIR}/json.hpp" "${tinygltf_SOURCE_DIR}/nlohmann/json.hpp" SYMBOLIC) + endif() + + # Set tinygltf to header-only mode + set(TINYGLTF_HEADER_ONLY ON CACHE BOOL "Use header only version" FORCE) + set(TINYGLTF_INSTALL OFF CACHE BOOL "Do not install tinygltf" FORCE) + + # Add the subdirectory after modifying the CMakeLists.txt + add_subdirectory(${tinygltf_SOURCE_DIR} ${tinygltf_BINARY_DIR}) + else() + # If already populated, just make it available + FetchContent_MakeAvailable(tinygltf) + endif() + + # Get the include directory from the target + get_target_property(tinygltf_INCLUDE_DIR tinygltf INTERFACE_INCLUDE_DIRECTORIES) + if(NOT tinygltf_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + FetchContent_GetProperties(tinygltf SOURCE_DIR tinygltf_SOURCE_DIR) + set(tinygltf_INCLUDE_DIR ${tinygltf_SOURCE_DIR}) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(tinygltf + REQUIRED_VARS tinygltf_INCLUDE_DIR +) + +if(tinygltf_FOUND) + set(tinygltf_INCLUDE_DIRS ${tinygltf_INCLUDE_DIR}) + + if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf::tinygltf INTERFACE IMPORTED) + set_target_properties(tinygltf::tinygltf PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${tinygltf_INCLUDE_DIRS}" + INTERFACE_COMPILE_DEFINITIONS "TINYGLTF_IMPLEMENTATION;TINYGLTF_NO_EXTERNAL_IMAGE;TINYGLTF_NO_STB_IMAGE;TINYGLTF_NO_STB_IMAGE_WRITE" + ) + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(tinygltf::tinygltf INTERFACE nlohmann_json::nlohmann_json) + endif() + endif() +elseif(TARGET tinygltf) + # If find_package_handle_standard_args failed but we have a tinygltf target from FetchContent + # Create an alias for the tinygltf target + if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf_wrapper INTERFACE) + target_link_libraries(tinygltf_wrapper INTERFACE tinygltf) + target_compile_definitions(tinygltf_wrapper INTERFACE + TINYGLTF_IMPLEMENTATION + TINYGLTF_NO_EXTERNAL_IMAGE + TINYGLTF_NO_STB_IMAGE + TINYGLTF_NO_STB_IMAGE_WRITE + ) + if(TARGET nlohmann_json::nlohmann_json) + target_link_libraries(tinygltf_wrapper INTERFACE nlohmann_json::nlohmann_json) + endif() + add_library(tinygltf::tinygltf ALIAS tinygltf_wrapper) + endif() + + # Set variables to indicate that tinygltf was found + set(tinygltf_FOUND TRUE) + set(TINYGLTF_FOUND TRUE) + + # Set include directories + get_target_property(tinygltf_INCLUDE_DIR tinygltf INTERFACE_INCLUDE_DIRECTORIES) + if(tinygltf_INCLUDE_DIR) + set(tinygltf_INCLUDE_DIRS ${tinygltf_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + FetchContent_GetProperties(tinygltf SOURCE_DIR tinygltf_SOURCE_DIR) + set(tinygltf_INCLUDE_DIR ${tinygltf_SOURCE_DIR}) + set(tinygltf_INCLUDE_DIRS ${tinygltf_INCLUDE_DIR}) + + # Explicitly set the include directory on the target + if(TARGET tinygltf) + set_target_properties(tinygltf PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${tinygltf_INCLUDE_DIR}" + ) + endif() +endif() +endif() + +mark_as_advanced(tinygltf_INCLUDE_DIR) diff --git a/CMake/Findtinyobjloader.cmake b/CMake/Findtinyobjloader.cmake new file mode 100644 index 0000000..4b1fb44 --- /dev/null +++ b/CMake/Findtinyobjloader.cmake @@ -0,0 +1,160 @@ +# Findtinyobjloader.cmake +# Find the tinyobjloader library +# +# This module defines the following variables: +# tinyobjloader_FOUND - True if tinyobjloader was found +# tinyobjloader_INCLUDE_DIRS - Include directories for tinyobjloader +# tinyobjloader_LIBRARIES - Libraries to link against tinyobjloader +# +# It also defines the following targets: +# tinyobjloader::tinyobjloader + +# Try to find the package using pkg-config first +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_tinyobjloader QUIET tinyobjloader) +endif() + +# Find the include directory +find_path(tinyobjloader_INCLUDE_DIR + NAMES tiny_obj_loader.h + PATHS + ${PC_tinyobjloader_INCLUDE_DIRS} + /usr/include + /usr/local/include + $ENV{VULKAN_SDK}/include + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/include + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../include + PATH_SUFFIXES tinyobjloader tiny_obj_loader +) + +# Find the library +find_library(tinyobjloader_LIBRARY + NAMES tinyobjloader + PATHS + ${PC_tinyobjloader_LIBRARY_DIRS} + /usr/lib + /usr/local/lib + $ENV{VULKAN_SDK}/lib + ${ANDROID_NDK}/sources/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../attachments/lib + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../external + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../third_party + ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../lib + PATH_SUFFIXES lib +) + +# If the include directory wasn't found, use FetchContent to download and build +if(NOT tinyobjloader_INCLUDE_DIR) + # If not found, use FetchContent to download and build + include(FetchContent) + + message(STATUS "tinyobjloader not found, fetching from GitHub...") + FetchContent_Declare( + tinyobjloader + GIT_REPOSITORY https://github.com/tinyobjloader/tinyobjloader.git + GIT_TAG v2.0.0rc10 # Use a specific tag for stability + ) + + # Set options before making tinyobjloader available + set(TINYOBJLOADER_BUILD_TEST_LOADER OFF CACHE BOOL "Do not build test loader" FORCE) + set(TINYOBJLOADER_BUILD_OBJ_STICHER OFF CACHE BOOL "Do not build obj sticher" FORCE) + set(TINYOBJLOADER_INSTALL OFF CACHE BOOL "Do not install tinyobjloader" FORCE) + + # Update CMake policy to suppress the deprecation warning + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() + + # Populate the content but don't configure it yet + FetchContent_GetProperties(tinyobjloader) + if(NOT tinyobjloader_POPULATED) + FetchContent_Populate(tinyobjloader) + + # Update the minimum required CMake version before including the CMakeLists.txt + file(READ "${tinyobjloader_SOURCE_DIR}/CMakeLists.txt" TINYOBJLOADER_CMAKE_CONTENT) + string(REPLACE "cmake_minimum_required(VERSION 3.2)" + "cmake_minimum_required(VERSION 3.10)" + TINYOBJLOADER_CMAKE_CONTENT "${TINYOBJLOADER_CMAKE_CONTENT}") + string(REPLACE "cmake_minimum_required(VERSION 3.5)" + "cmake_minimum_required(VERSION 3.10)" + TINYOBJLOADER_CMAKE_CONTENT "${TINYOBJLOADER_CMAKE_CONTENT}") + file(WRITE "${tinyobjloader_SOURCE_DIR}/CMakeLists.txt" "${TINYOBJLOADER_CMAKE_CONTENT}") + + # Now add the subdirectory manually + add_subdirectory(${tinyobjloader_SOURCE_DIR} ${tinyobjloader_BINARY_DIR}) + else() + # If already populated, just make it available + FetchContent_MakeAvailable(tinyobjloader) + endif() + + # Get the include directory from the target + get_target_property(tinyobjloader_INCLUDE_DIR tinyobjloader INTERFACE_INCLUDE_DIRECTORIES) + if(NOT tinyobjloader_INCLUDE_DIR) + # If we can't get the include directory from the target, use the source directory + set(tinyobjloader_INCLUDE_DIR ${tinyobjloader_SOURCE_DIR}) + endif() +endif() + +# Set the variables +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(tinyobjloader + REQUIRED_VARS tinyobjloader_INCLUDE_DIR +) + +if(tinyobjloader_FOUND) + set(tinyobjloader_INCLUDE_DIRS ${tinyobjloader_INCLUDE_DIR}) + + if(tinyobjloader_LIBRARY) + set(tinyobjloader_LIBRARIES ${tinyobjloader_LIBRARY}) + else() + # tinyobjloader is a header-only library, so no library is needed + set(tinyobjloader_LIBRARIES "") + endif() + + # Create an imported target + if(NOT TARGET tinyobjloader::tinyobjloader) + add_library(tinyobjloader::tinyobjloader INTERFACE IMPORTED) + set_target_properties(tinyobjloader::tinyobjloader PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${tinyobjloader_INCLUDE_DIRS}" + ) + if(tinyobjloader_LIBRARIES) + set_target_properties(tinyobjloader::tinyobjloader PROPERTIES + INTERFACE_LINK_LIBRARIES "${tinyobjloader_LIBRARIES}" + ) + endif() + endif() +elseif(TARGET tinyobjloader) + # If find_package_handle_standard_args failed but we have a tinyobjloader target from FetchContent + # Create an alias for the tinyobjloader target + if(NOT TARGET tinyobjloader::tinyobjloader) + add_library(tinyobjloader::tinyobjloader ALIAS tinyobjloader) + endif() + + # Set variables to indicate that tinyobjloader was found + set(tinyobjloader_FOUND TRUE) + set(TINYOBJLOADER_FOUND TRUE) + + # Set include directories + get_target_property(tinyobjloader_INCLUDE_DIR tinyobjloader INTERFACE_INCLUDE_DIRECTORIES) + if(tinyobjloader_INCLUDE_DIR) + set(tinyobjloader_INCLUDE_DIRS ${tinyobjloader_INCLUDE_DIR}) + else() + # If we can't get the include directory from the target, use the source directory + set(tinyobjloader_INCLUDE_DIR ${tinyobjloader_SOURCE_DIR}) + set(tinyobjloader_INCLUDE_DIRS ${tinyobjloader_INCLUDE_DIR}) + endif() +endif() + +mark_as_advanced(tinyobjloader_INCLUDE_DIR tinyobjloader_LIBRARY) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..636970f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,202 @@ +cmake_minimum_required (VERSION 3.29) + +project (VulkanTutorial) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMake") + +# Add option to enable/disable C++ 20 module +option(ENABLE_CPP20_MODULE "Enable C++ 20 module support for Vulkan" OFF) + +# Enable C++ module dependency scanning only if C++ 20 module is enabled +if(ENABLE_CPP20_MODULE) + set(CMAKE_CXX_SCAN_FOR_MODULES ON) +endif() + +find_package (glfw3 REQUIRED) +find_package (glm REQUIRED) +find_package (Vulkan 1.4.335 REQUIRED) # Require Vulkan SDK version 1.4.335 or higher +find_package (tinyobjloader REQUIRED) +find_package (tinygltf REQUIRED) +find_package (KTX REQUIRED) + +# set up Vulkan C++ module only if enabled +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) + + # Add MSVC-specific compiler options for proper C++ module support + if(MSVC) + target_compile_options(VulkanCppModule PRIVATE + /std:c++latest # Use latest C++ standard for better module support + /permissive- # Standards conformance mode + /Zc:__cplusplus # Enable correct __cplusplus macro + /EHsc # Enable C++ exception handling + /Zc:preprocessor # Use conforming preprocessor + /translateInclude # Automatically translate #include to import for standard library + ) + endif() + + target_sources(VulkanCppModule + PUBLIC + FILE_SET cxx_modules TYPE CXX_MODULES + BASE_DIRS + "${Vulkan_INCLUDE_DIR}" + FILES + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" + ) + + + # Add the vulkan.cppm file directly as a source file + target_sources(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" + ) +else() + # Create a dummy interface library when C++ 20 module is disabled + 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() + +find_package(stb REQUIRED) +set(STB_INCLUDEDIR ${stb_INCLUDE_DIRS}) + +add_executable (glslang::validator IMPORTED) +find_program (GLSLANG_VALIDATOR "glslangValidator" HINTS $ENV{VULKAN_SDK}/bin REQUIRED) +set_property (TARGET glslang::validator PROPERTY IMPORTED_LOCATION "${GLSLANG_VALIDATOR}") +find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin REQUIRED) + +include(CheckIPOSupported) +check_ipo_supported(RESULT LTO_SUPPORTED OUTPUT LTO_ERROR) + +if(LTO_SUPPORTED AND CMAKE_BUILD_TYPE STREQUAL "Release") + message(STATUS "LTO activated") + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) +else() + message(WARNING "LTO not supported : ${LTO_ERROR}") +endif() + +function (add_shaders_target TARGET) + cmake_parse_arguments ("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN}) + set (SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders) + add_custom_command ( + OUTPUT ${SHADERS_DIR} + COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR} + ) + add_custom_command ( + OUTPUT ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv + COMMAND glslang::validator + ARGS --target-env vulkan1.0 ${SHADER_SOURCES} --quiet + WORKING_DIRECTORY ${SHADERS_DIR} + DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} + COMMENT "Compiling Shaders" + VERBATIM + ) + add_custom_target (${TARGET} DEPENDS ${SHADERS_DIR}/frag.spv ${SHADERS_DIR}/vert.spv) +endfunction () + +function (add_slang_shader_target TARGET) + cmake_parse_arguments ("SHADER" "" "CHAPTER_NAME" "SOURCES" ${ARGN}) + set (SHADERS_DIR ${SHADER_CHAPTER_NAME}/shaders) + #file(GLOB HAS_COMPUTE ${CHAPTER_SHADER}.comp) + set (ENTRY_POINTS -entry vertMain -entry fragMain) + if(HAS_COMPUTE) + list(APPEND ENTRY_POINTS -entry compMain) + endif() + add_custom_command ( + OUTPUT ${SHADERS_DIR} + COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADERS_DIR} + ) + add_custom_command ( + OUTPUT ${SHADERS_DIR}/slang.spv + COMMAND ${SLANGC_EXECUTABLE} ${SHADER_SOURCES} -target spirv -profile spirv_1_4+spvRayQueryKHR -emit-spirv-directly -fvk-use-entrypoint-name ${ENTRY_POINTS} -o slang.spv + WORKING_DIRECTORY ${SHADERS_DIR} + DEPENDS ${SHADERS_DIR} ${SHADER_SOURCES} + COMMENT "Compiling Slang Shaders" + VERBATIM + ) + add_custom_target (${TARGET} DEPENDS ${SHADERS_DIR}/slang.spv) +endfunction() + +function (add_chapter CHAPTER_NAME) + cmake_parse_arguments (CHAPTER "" "SHADER" "LIBS;TEXTURES;MODELS" ${ARGN}) + add_executable (${CHAPTER_NAME} src/${CHAPTER_NAME}.cpp) + set_target_properties (${CHAPTER_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CHAPTER_NAME}) + set_target_properties (${CHAPTER_NAME} PROPERTIES CXX_STANDARD 20) + target_link_libraries (${CHAPTER_NAME} Vulkan::cppm glfw) + target_include_directories (${CHAPTER_NAME} PRIVATE ${STB_INCLUDEDIR}) + + # Add compile definition if C++ 20 module is enabled + if(ENABLE_CPP20_MODULE) + target_compile_definitions(${CHAPTER_NAME} PRIVATE USE_CPP20_MODULES=1) + endif() + + # Define VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS to treat VK_ERROR_OUT_OF_DATE_KHR as a success code + target_compile_definitions(${CHAPTER_NAME} PRIVATE "VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS" ) + + if(WIN32) + if(${CMAKE_GENERATOR} MATCHES "Visual Studio.*") + set_target_properties(${CHAPTER_NAME} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/${CHAPTER_NAME}") + endif() + endif() + + if (DEFINED CHAPTER_SHADER) + set (CHAPTER_SHADER_TARGET ${CHAPTER_NAME}_shader) + file (GLOB SHADER_SOURCES ${CHAPTER_SHADER}.frag ${CHAPTER_SHADER}.vert ${CHAPTER_SHADER}.comp) + if (SHADER_SOURCES) + add_shaders_target (${CHAPTER_SHADER_TARGET} CHAPTER_NAME ${CHAPTER_NAME} SOURCES ${SHADER_SOURCES}) + add_dependencies (${CHAPTER_NAME} ${CHAPTER_SHADER_TARGET}) + endif() + + set (CHAPTER_SHADER_SLANG_TARGET ${CHAPTER_NAME}_slang_shader) + file (GLOB SHADER_SLANG_SOURCES ${CHAPTER_SHADER}.slang) + if(SHADER_SLANG_SOURCES) + add_slang_shader_target( ${CHAPTER_SHADER_SLANG_TARGET} CHAPTER_NAME ${CHAPTER_NAME} SOURCES ${SHADER_SLANG_SOURCES}) + add_dependencies(${CHAPTER_NAME} ${CHAPTER_SHADER_SLANG_TARGET}) + endif() + endif () + if (DEFINED CHAPTER_LIBS) + target_link_libraries (${CHAPTER_NAME} ${CHAPTER_LIBS}) + endif () + if (DEFINED CHAPTER_MODELS) + list(TRANSFORM CHAPTER_MODELS PREPEND "${CMAKE_SOURCE_DIR}/assets/") + file (COPY ${CHAPTER_MODELS} DESTINATION ${CMAKE_BINARY_DIR}/${CHAPTER_NAME}/models) + endif () + if (DEFINED CHAPTER_TEXTURES) + list(TRANSFORM CHAPTER_TEXTURES PREPEND "${CMAKE_SOURCE_DIR}/textures/") + file (COPY ${CHAPTER_TEXTURES} DESTINATION ${CMAKE_BINARY_DIR}/${CHAPTER_NAME}/textures) + endif () +endfunction () + +add_chapter (main + SHADER shaders/main + TEXTURES texture.jpg +) + +#add_chapter (38_ray_tracing +# SHADER 38_ray_tracing +# MODELS plant_on_table.obj +# MODELS plant_on_table.mtl +# TEXTURES plant_on_table_textures/nettle_plant_diff_4k.png +# TEXTURES plant_on_table_textures/potted_plant_02_pot_diff_1k.png +# TEXTURES plant_on_table_textures/wooden_picnic_table_bottom_diff_1k.png +# TEXTURES plant_on_table_textures/wooden_picnic_table_top_diff_1k.png +# LIBS glm::glm tinyobjloader::tinyobjloader) diff --git a/compile_shaders.sh b/compile_shaders.sh new file mode 100755 index 0000000..d4b7a61 --- /dev/null +++ b/compile_shaders.sh @@ -0,0 +1 @@ +slangc shaders/main.slang -target spirv -profile spirv_1_4 -emit-spirv-directly -fvk-use-entrypoint-name -entry vertMain -entry fragMain -o shaders/slang.spv diff --git a/shaders/main.slang b/shaders/main.slang new file mode 100644 index 0000000..14c8123 --- /dev/null +++ b/shaders/main.slang @@ -0,0 +1,34 @@ +struct VSInput { + float3 inPosition; + float3 inColor; + float2 inTexCoord; +}; + +struct UniformBuffer { + float4x4 model; + float4x4 view; + float4x4 proj; +}; +ConstantBuffer ubo; + +struct VSOutput { + float4 pos : SV_Position; + float3 fragColor; + float2 fragTexCoord; +}; + +[shader("vertex")] +VSOutput vertMain(VSInput input) { + VSOutput output; + output.pos = mul(ubo.proj, mul(ubo.view, mul(ubo.model, float4(input.inPosition, 1.0)))); + output.fragColor = input.inColor; + output.fragTexCoord = input.inTexCoord; + return output; +} + +Sampler2D texture; + +[shader("fragment")] +float4 fragMain(VSOutput vertIn) : SV_Target { + return texture.Sample(vertIn.fragTexCoord); +} diff --git a/shaders/shader.slang b/shaders/shader.slang new file mode 100644 index 0000000..7de09a7 --- /dev/null +++ b/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/shaders/slang.spv b/shaders/slang.spv new file mode 100644 index 0000000000000000000000000000000000000000..87c2941386d6184ed90854503b1cd55511e73820 GIT binary patch literal 896 zcmZ9JPfNo<5XHx~X=?@BTK|BGB&blW_9%j=2M^*w$TyH6RRWqwOa*U#Y%hZEx6MN6 zhRM#Goqh9Ww%IuBSZl@1#x~WTHME8iS+}Oc_jk_AxJ~X%^?7X_@A(Mqq8n?a%J=x4dw7 zdam{?D~szh6=5(BX>>xL6OA-^(K*<+JbXiep@-AY)5>|6wLg=-3s%H;C_XfMJPPf zjyB>GVeW3qSRQXV^m9x8o_zA*1&7+$p$>89e`^>~+ z@|j6JpP6uep*J8-FDzoeLiB#EKRpd()Q{B}8b?)(9_fobwC>8#bt4w^kcW>J&i+*C hQs!%p6=V6DC-OblseClY(nrlR88Pa?iGE_9^B?GhN%;T( literal 0 HcmV?d00001 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ed86d62 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,1593 @@ +//========== Includes ========== +// General includes +#include +#include +#include +#include // std::clamp +#include +#include +#include // uint32_t +#include +#include // std::numeric_limits +#include // to load shaders +#include +#include +#include +#include +#include +// Vulkan Includes +#include "vulkan/vulkan.hpp" +#include +#include +#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES) +#include +#else +import vulkan_hpp; +#endif +// GLFW Includes +#define GLFW_INCLUDE_VULKAN +#include +// GLM - Linear algebra +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +#include +// Images +#define STB_IMAGE_IMPLEMENTATION +#include + +// Consts +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 + +// Vertices +struct Vertex { + glm::vec3 pos; + glm::vec3 color; + glm::vec2 texCoord; + + static vk::VertexInputBindingDescription getBindingDescription() { + return { 0, sizeof(Vertex), vk::VertexInputRate::eVertex }; + } + + static std::array getAttributeDescriptions() { + return { + vk::VertexInputAttributeDescription( 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) ), + vk::VertexInputAttributeDescription( 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) ), + vk::VertexInputAttributeDescription( 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord) ) + }; + } +}; + +const std::vector vertices = { + {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}, + + {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, + {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}} +}; + +const std::vector indices = { + 0, 1, 2, 2, 3, 0, + 4, 5, 6, 6, 7, 4 +}; + + +//Uniform buffer +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +class HelloTriangleApplication { + public: + void run() { + #ifdef DEBUG + std::cout << "Global initialisation...\n"; + #endif + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); + #ifdef DEBUG + std::cout << "Bye Bye !\n"; + #endif + + } + private: + //========== CLASS MEMBERS ========== + GLFWwindow *window = nullptr; + vk::raii::Context context; + vk::raii::Instance instance = nullptr; + vk::raii::DebugUtilsMessengerEXT debugMessenger = nullptr; + vk::raii::SurfaceKHR surface = nullptr; + vk::raii::PhysicalDevice physicalDevice = nullptr; + vk::raii::Device device = nullptr; + vk::raii::Queue queue = nullptr; + // Swap chain + vk::raii::SwapchainKHR swapChain = nullptr; + std::vector swapChainImages; + vk::Format swapChainImageFormat = vk::Format::eUndefined; + vk::Extent2D swapChainExtent; + std::vector swapChainImageViews; + // Decriptor Set (used for Uniform buffers) + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + // Pools + vk::raii::CommandPool commandPool = nullptr; + vk::raii::DescriptorPool descriptorPool = nullptr; + std::vector descriptorSets; + // Pipeline + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline graphicsPipeline = nullptr; + // Buffers + // Vertices - Buffers + std::vector commandBuffers; + vk::raii::Buffer vertexBuffer = nullptr; + vk::raii::DeviceMemory vertexBufferMemory = nullptr; + // Index - Buffers + vk::raii::Buffer indexBuffer = nullptr; + vk::raii::DeviceMemory indexBufferMemory = nullptr; + // Uniform Buffers - Buffers + std::vector uniformBuffers; + std::vector uniformBuffersMemory; + std::vector uniformBuffersMapped; + // Sync + std::vector presentCompleteSemaphores; + std::vector renderFinishedSemaphores; + std::vector inFlightFences; + // Image texture + vk::raii::Image textureImage = nullptr; + vk::raii::DeviceMemory textureImageMemory = nullptr; + vk::raii::ImageView textureImageView = nullptr; + vk::raii::Sampler textureSampler = nullptr; + // Depth images + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthImageMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + // Indexes + uint32_t queueIndex = ~0; + uint32_t frameIndex = 0; + // Bools + bool framebufferResized = false; + + // Device Extensions + std::vector deviceExtensions = { + vk::KHRSwapchainExtensionName + }; + + //========== Inits ========== + + void initWindow() { + #ifdef DEBUG + std::cout << "Creating GLFW window...\n"; + #endif + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); + glfwSetWindowUserPointer(window, this); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); + #ifdef DEBUG + std::cout << "Window created !\n"; + #endif + } + + void initVulkan() { + #ifdef DEBUG + std::cout << "\tInitialing Vulkan...\n"; + #endif + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createDescriptorSetLayout(); + createGraphicsPipeline(); + createCommandPool(); + createDepthResources(); + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + createDescriptorPool(); + createDescriptorSets(); + createCommandBuffers(); + createSyncObjects(); + #ifdef DEBUG + std::cout << "\tVulkan is initialised !\n"; + #endif + } + + void mainLoop() { + #ifdef DEBUG + std::cout << "Main Loop...\n"; + #endif + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + device.waitIdle(); + } + + void cleanupSwapChain() { + swapChainImageViews.clear(); + swapChain = nullptr; + } + + void cleanup() { + #ifdef DEBUG + std::cout << "Cleaning up...\n"; + #endif + cleanupSwapChain(); + surface = nullptr; + glfwDestroyWindow(window); + glfwTerminate(); + #ifdef DEBUG + std::cout << "Done !\n"; + #endif + } + + + //========== UTILS ========== + void createInstance() { + #ifdef DEBUG + std::cout << "Creating a Vulkan Instance...\n"; + #endif + constexpr vk::ApplicationInfo appInfo{ + .pApplicationName = "Hello Vulkan", + .applicationVersion = VK_MAKE_VERSION( 1, 0, 0), + .pEngineName = "No Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = vk::ApiVersion14 }; + + // Get the required leyers + std::vector requiredLayers; + if (enableValidationLayers) { + requiredLayers.assign(validationLayers.begin(), validationLayers.end()); + } + + // Check if the required layers are supported by the Vulkan implementation + auto layerProperties = 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 are not supported!\nAborting..."); + } + + // Get the required extensions + auto requiredExtensions = getRequiredExtensions(); + + // Check if the required extensions are supported by the Vulkan implementation + auto extensionsProperties = context.enumerateInstanceExtensionProperties(); + for (auto const &requiredExtension : requiredExtensions) + { + if (std::ranges::none_of(extensionsProperties, + [requiredExtension](auto const &extensionsProperty) + { return strcmp(extensionsProperty.extensionName, requiredExtension) == 0; })) + { + throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension) + "\nAborting..."); + } + } + + vk::InstanceCreateInfo createInfo{ + .pApplicationInfo = &appInfo, + .enabledLayerCount = static_cast(requiredLayers.size()), + .ppEnabledLayerNames = requiredLayers.data(), + .enabledExtensionCount = static_cast(requiredExtensions.size()), + .ppEnabledExtensionNames = requiredExtensions.data()}; + + instance = vk::raii::Instance(context, createInfo); + #ifdef DEBUG + std::cout << requiredExtensions.size() << " extensions loaded !\n"; + std::cout << requiredLayers.size() << " layers loaded !\n"; + + std::cout << "Finished creating the Vulkan Instance !\n"; + #endif + } + + void setupDebugMessenger () { + #ifdef DEBUG + std::cout << "Setting up the messenger !\n"; + #endif + if (!enableValidationLayers) { + return; + } + + vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError); + vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags(vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation); + vk::DebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfoEXT{ + .messageSeverity = severityFlags, + .messageType = messageTypeFlags, + .pfnUserCallback = &debugCallback }; + + debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT); + #ifdef DEBUG + std::cout << "Messenger set up !\n"; + #endif + } + + void pickPhysicalDevice() { + #ifdef DEBUG + std::cout << "Selecting the GPU...\n"; + #endif + std::vector devices = instance.enumeratePhysicalDevices(); + const auto devIter = std::ranges::find_if( + devices, + [&](auto const &device) { + // Check if the device supports the Vulkan 1.3 API version + bool supportsVulkan1_3 = device.getProperties().apiVersion >= VK_API_VERSION_1_3; + + // Check if any of the queue families support graphics operations + auto queueFamilies = device.getQueueFamilyProperties(); + bool supportsGraphics = std::ranges::any_of(queueFamilies, [](auto const &qfp) { return !!(qfp.queueFlags & vk::QueueFlagBits::eGraphics); }); + return supportsVulkan1_3 && supportsGraphics; + }); + if (devIter != devices.end()) { + physicalDevice = *devIter; + #ifdef DEBUG + std::cout << "GPU selected !\n"; + #endif + } + else { + throw std::runtime_error("Failed to find a suitable GPU!\nAborting..."); + } + } + + + void createLogicalDevice() { + #ifdef DEBUG + std::cout << "Creating a logical device !\n"; + #endif + + //std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + uint32_t queueIndex = findQueueFamilies(physicalDevice); // This is an index + + + vk::StructureChain< + vk::PhysicalDeviceFeatures2, + vk::PhysicalDeviceVulkan13Features, + vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT + > featureChain = { + {.features = {.samplerAnisotropy = true}}, + { + .synchronization2 = true, + .dynamicRendering = true, + }, // All features from Vulkan 1.3 + { .extendedDynamicState = true } + }; + + + // Create the device: + float queuePriority = 0.5f; + vk::DeviceQueueCreateInfo deviceQueueCreateInfo { + .queueFamilyIndex = queueIndex, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + vk::DeviceCreateInfo deviceCreateInfo { + .pNext = &featureChain.get(), + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &deviceQueueCreateInfo, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data() + }; + + device = vk::raii::Device( physicalDevice, deviceCreateInfo ); + queue = vk::raii::Queue( device, queueIndex, 0 ); + #ifdef DEBUG + std::cout << "Logical device created\n"; + #endif + } + + void createSurface() { + #ifdef DEBUG + std::cout << "Creating a surface !\n"; + #endif + VkSurfaceKHR _surface; + if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) { + throw std::runtime_error("Failed to create Window surface !\nAborting..."); + } + surface = vk::raii::SurfaceKHR(instance, _surface); + #ifdef DEBUG + std::cout << "Surface created !\n"; + #endif + } + + void createSwapChain() { + #ifdef DEBUG + std::cout << "Creating Swap Chain...\n"; + #endif + auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(*surface); + auto swapChainSurfaceFormat = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR(*surface)); + swapChainExtent = chooseSwapExtent(surfaceCapabilities); + auto minImageCount = std::max(3u, surfaceCapabilities.minImageCount); + minImageCount = ( surfaceCapabilities.maxImageCount > 0 && minImageCount > surfaceCapabilities.maxImageCount ) ? surfaceCapabilities.maxImageCount : minImageCount; + + vk::SwapchainCreateInfoKHR swapChainCreateInfo { + .flags = vk::SwapchainCreateFlagsKHR(), + .surface = *surface, + .minImageCount = minImageCount, + .imageFormat = swapChainSurfaceFormat.format, + .imageColorSpace = swapChainSurfaceFormat.colorSpace, + .imageExtent = swapChainExtent, + .imageArrayLayers = 1, + .imageUsage = vk::ImageUsageFlagBits::eColorAttachment, + .imageSharingMode = vk::SharingMode::eExclusive, + .preTransform = surfaceCapabilities.currentTransform, + .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque, + .presentMode = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR(*surface)), + .clipped = true, + .oldSwapchain = nullptr + }; + swapChain = vk::raii::SwapchainKHR(device, swapChainCreateInfo); + swapChainImages = swapChain.getImages(); + swapChainImageFormat = swapChainSurfaceFormat.format; + #ifdef DEBUG + std::cout << "Swap Chain Created !\n"; + #endif + } + + void createImageViews() { + #ifdef DEBUG + std::cout << "Creating Image View...\n"; + #endif + swapChainImageViews.clear(); + + for (uint32_t i = 0; i < swapChainImages.size(); i++) { + swapChainImageViews.emplace_back(createImageView( + swapChainImages[i], + swapChainImageFormat, + vk::ImageAspectFlagBits::eColor + )); + } + #ifdef DEBUG + std::cout << "Image View created !\n"; + #endif + } + + void createDescriptorSetLayout() { + #ifdef DEBUG + std::cout << "Creating the descriptor set...\n"; + #endif + std::array bindings = { + vk::DescriptorSetLayoutBinding( + 0, + vk::DescriptorType::eUniformBuffer, + 1, + vk::ShaderStageFlagBits::eVertex, + nullptr + ), + vk::DescriptorSetLayoutBinding( + 1, + vk::DescriptorType::eCombinedImageSampler, + 1, + vk::ShaderStageFlagBits::eFragment, + nullptr + ) + }; + vk::DescriptorSetLayoutCreateInfo layoutInfo { + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + #ifdef DEBUG + std::cout << "Descriptor set created !\n"; + #endif + } + + void createGraphicsPipeline() { + #ifdef DEBUG + std::cout << "Creating the Graphics Pipeline...\n"; + #endif + vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv")); + + vk::PipelineShaderStageCreateInfo vertShaderStageInfo { + .stage = vk::ShaderStageFlagBits::eVertex, + .module = shaderModule, + .pName = "vertMain" + }; + vk::PipelineShaderStageCreateInfo fragShaderStageInfo { + .stage = vk::ShaderStageFlagBits::eFragment, + .module = shaderModule, + .pName = "fragMain" + }; + vk::PipelineShaderStageCreateInfo shaderStages[] { + vertShaderStageInfo, + fragShaderStageInfo + }; + + // Passing vertices into the pipeline + auto bindingDescription = Vertex::getBindingDescription(); + auto attributeDescription = Vertex::getAttributeDescriptions(); + + vk::PipelineVertexInputStateCreateInfo vertexInputInfo { + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &bindingDescription, + .vertexAttributeDescriptionCount = static_cast( attributeDescription.size() ), + .pVertexAttributeDescriptions = attributeDescription.data() + }; + + vk::PipelineInputAssemblyStateCreateInfo inputAssembly { + .topology = vk::PrimitiveTopology::eTriangleList + }; + + vk::PipelineViewportStateCreateInfo viewportState { + .viewportCount = 1, + .scissorCount = 1 + }; + + vk::PipelineRasterizationStateCreateInfo rasterizer { + .depthClampEnable = vk::False, + .rasterizerDiscardEnable = vk::False, + .polygonMode = vk::PolygonMode::eFill, + .cullMode = vk::CullModeFlagBits::eBack, + .frontFace = vk::FrontFace::eCounterClockwise, + .depthBiasEnable = vk::False, + .depthBiasSlopeFactor = 1.0f, + .lineWidth = 1.0f + }; + + vk::PipelineMultisampleStateCreateInfo multisampling { + .rasterizationSamples = vk::SampleCountFlagBits::e1, + .sampleShadingEnable = vk::False + }; + + vk::PipelineDepthStencilStateCreateInfo depthStencil { + .depthTestEnable = vk::True, + .depthWriteEnable = vk::True, + .depthCompareOp = vk::CompareOp::eLess, + .depthBoundsTestEnable = vk::False, + .stencilTestEnable = vk::False + }; + + // Color blending + vk::PipelineColorBlendAttachmentState colorBlendAttachment { + .blendEnable = vk::False, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA + }; + + vk::PipelineColorBlendStateCreateInfo colorBlending { + .logicOpEnable = vk::False, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &colorBlendAttachment + }; + + // Dynamic States + std::vector dynamicStates = { + vk::DynamicState::eViewport, + vk::DynamicState::eScissor + }; + + vk::PipelineDynamicStateCreateInfo dynamicState { + .dynamicStateCount = static_cast(dynamicStates.size()), + .pDynamicStates = dynamicStates.data() + }; + + vk::PipelineLayoutCreateInfo pipelineLayoutInfo { + .setLayoutCount = 1, + .pSetLayouts = &*descriptorSetLayout, + .pushConstantRangeCount = 0 + }; + + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + vk::Format depthFormat = findDepthFormat(); + + vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo { + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &swapChainImageFormat, + .depthAttachmentFormat = depthFormat + }; + + // Pipeline creation + vk::GraphicsPipelineCreateInfo pipelineInfo { + .pNext = &pipelineRenderingCreateInfo, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencil, + .pColorBlendState = &colorBlending, + .pDynamicState = &dynamicState, + .layout = pipelineLayout, + .renderPass = nullptr, + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = -1 + }; + + graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo); + + #ifdef DEBUG + std::cout << "Pipeline created !\n"; + #endif + } + + void createCommandPool() { + #ifdef DEBUG + std::cout << "Creating a command pool\n"; + #endif + vk::CommandPoolCreateInfo poolInfo { + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + .queueFamilyIndex = queueIndex + }; + + commandPool = vk::raii::CommandPool(device, poolInfo); + #ifdef DEBUG + std::cout << "Command pool created\n"; + #endif + } + + void createDepthResources() { + #ifdef DEBUG + std::cout << "Creating a depth ressouce...\n"; + #endif + vk::Format depthFormat = findDepthFormat(); + + createImage( + swapChainExtent.width, + swapChainExtent.height, + depthFormat, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eDepthStencilAttachment, + vk::MemoryPropertyFlagBits::eDeviceLocal, + depthImage, + depthImageMemory + ); + depthImageView = createImageView( + depthImage, + depthFormat, + vk::ImageAspectFlagBits::eDepth + ); + #ifdef DEBUG + std::cout << "Depth ressouce created !\n"; + #endif + } + + void createTextureImage(){ + #ifdef DEBUG + std::cout << "Creating a texture image...\n"; + #endif + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + vk::DeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("Failed to load texture image!\nAborting..."); + } + // Temporary buffers + vk::raii::Buffer stagingBuffer( {} ); + vk::raii::DeviceMemory stagingBufferMemory( {} ); + createBuffer( + imageSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + stagingBuffer, + stagingBufferMemory + ); + + void* data = stagingBufferMemory.mapMemory(0, imageSize); + memcpy(data, pixels, imageSize); + stagingBufferMemory.unmapMemory(); + stbi_image_free(pixels); + + createImage( + texWidth, + texHeight, + vk::Format::eR8G8B8A8Srgb, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, + vk::MemoryPropertyFlagBits::eDeviceLocal, + textureImage, + textureImageMemory + ); + + transitionImageLayout(textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); + transitionImageLayout(textureImage, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); + + #ifdef DEBUG + std::cout << "Texture image created !\n"; + #endif + } + + void createTextureImageView() { + #ifdef DEBUG + std::cout << "Creating the texture image...\n"; + #endif + textureImageView = createImageView( + textureImage, + vk::Format::eR8G8B8A8Srgb, + vk::ImageAspectFlagBits::eColor + ); + #ifdef DEBUG + std::cout << "Texture Image created !\n"; + #endif + } + + void createImage( + uint32_t width, + uint32_t height, + vk::Format format, + vk::ImageTiling tiling, + vk::ImageUsageFlags usage, + vk::MemoryPropertyFlags properties, + vk::raii::Image &image, + vk::raii::DeviceMemory &imageMemory + ) { + #ifdef DEBUG + std::cout << "Creating an image with usage " << vk::to_string(usage) << '\n'; + #endif + vk::ImageCreateInfo imageInfo { + .imageType = vk::ImageType::e2D, + .format = format, + .extent = { width, height, 1 }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = tiling, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + image = vk::raii::Image(device, imageInfo); + + vk::MemoryRequirements memRequirements = image.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo { + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + imageMemory = vk::raii::DeviceMemory(device, allocInfo); + image.bindMemory(imageMemory, 0); + #ifdef DEBUG + std::cout << "Created image with usage " << vk::to_string(usage) << '\n'; + #endif + } + + void createTextureSampler() { + #ifdef DEBUG + std::cout << "Creating a texture sampler...\n"; + #endif + vk::PhysicalDeviceProperties properties = physicalDevice.getProperties(); + vk::SamplerCreateInfo samplerInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .mipLodBias = 0.0f, + .anisotropyEnable = vk::True, + .maxAnisotropy = properties.limits.maxSamplerAnisotropy, + .compareEnable = vk::True, + .compareOp = vk::CompareOp::eAlways, + .minLod = 0.0f, + .maxLod = 0.0f, + .borderColor = vk::BorderColor::eIntOpaqueBlack, + .unnormalizedCoordinates = vk::False + }; + + textureSampler = vk::raii::Sampler(device, samplerInfo); + + #ifdef DEBUG + std::cout << "Texture sampler created !\n"; + #endif + } + + void createVertexBuffer() { + #ifdef DEBUG + std::cout << "Creating a vertex buffer...\n"; + #endif + vk::raii::Buffer stagingBuffer = nullptr; + vk::raii::DeviceMemory stagingBufferMemory = nullptr; + vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + stagingBuffer, + stagingBufferMemory + ); + + void* dataStaging = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(dataStaging, vertices.data(), (size_t)bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst, + vk::MemoryPropertyFlagBits::eDeviceLocal, + vertexBuffer, + vertexBufferMemory + ); + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + #ifdef DEBUG + std::cout << "Vertex buffer created !\n"; + #endif + } + + void copyBuffer( + vk::raii::Buffer & srcBuffer, + vk::raii::Buffer & dstBuffer, + vk::DeviceSize size + ) { + #ifdef DEBUG + std::cout << "Copying buffer " << vk::to_string(srcBuffer.objectType) << " to " << vk::to_string(dstBuffer.objectType) << "...\n"; + #endif + vk::raii::CommandBuffer commandCopyBuffer = beginSingleTimeCommands(); + + commandCopyBuffer.copyBuffer(srcBuffer, dstBuffer, vk::BufferCopy(0, 0, size)); + + endSingleTimeCommands(commandCopyBuffer); + + #ifdef DEBUG + std::cout << "Copied buffer " << vk::to_string(srcBuffer.objectType) << " to " << vk::to_string(dstBuffer.objectType) << "!\n"; + #endif + } + + void createIndexBuffer() { + #ifdef DEBUG + std::cout << "Creating an index buffer...\n"; + #endif + vk::DeviceSize bufferSize = sizeof( indices[0] ) * indices.size(); + + vk::raii::Buffer stagingBuffer( {} ); + vk::raii::DeviceMemory stagingBufferMemory( {} ); + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + stagingBuffer, + stagingBufferMemory + ); + + void* data = stagingBufferMemory.mapMemory(0, bufferSize); + memcpy(data, indices.data(), (size_t) bufferSize); + stagingBufferMemory.unmapMemory(); + + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal, + indexBuffer, + indexBufferMemory + ); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + #ifdef DEBUG + std::cout << "Index buffer created !\n"; + #endif + } + + void createUniformBuffers() { + #ifdef DEBUG + std::cout << "Creating uniform buffers...\n"; + #endif + uniformBuffers.clear(); + uniformBuffersMemory.clear(); + uniformBuffersMapped.clear(); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + vk::raii::Buffer buffer( {} ); + vk::raii::DeviceMemory bufferMem( {} ); + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eUniformBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + buffer, + bufferMem + ); + + uniformBuffers.emplace_back(std::move(buffer)); + uniformBuffersMemory.emplace_back(std::move(bufferMem)); + uniformBuffersMapped.emplace_back( uniformBuffersMemory[i].mapMemory(0, bufferSize)); + } + #ifdef DEBUG + std::cout << "Uniform buffers created !\n"; + #endif + } + + void createDescriptorPool() { + #ifdef DEBUG + std::cout << "Creating a descriptor pool...\n"; + #endif + std::array poolSize { + vk::DescriptorPoolSize( + vk::DescriptorType::eUniformBuffer, + MAX_FRAMES_IN_FLIGHT + ), + vk::DescriptorPoolSize( + vk::DescriptorType::eCombinedImageSampler, + MAX_FRAMES_IN_FLIGHT + ) + }; + vk::DescriptorPoolCreateInfo poolInfo { + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = MAX_FRAMES_IN_FLIGHT, + .poolSizeCount = static_cast(poolSize.size()), + .pPoolSizes = poolSize.data() + }; + + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + + #ifdef DEBUG + std::cout << "Descriptor pool created !\n"; + #endif + } + + void createDescriptorSets() { + #ifdef DEBUG + std::cout << "Creating a descriptor set...\n"; + #endif + std::vector layouts( MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout ); + vk::DescriptorSetAllocateInfo allocInfo { + .descriptorPool = descriptorPool, + .descriptorSetCount = static_cast(layouts.size()), + .pSetLayouts = layouts.data() + }; + + descriptorSets.clear(); + descriptorSets = device.allocateDescriptorSets(allocInfo); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vk::DescriptorBufferInfo bufferInfo { + .buffer = uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + vk::DescriptorImageInfo imageInfo { + .sampler = textureSampler, + .imageView = textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + std::array descriptorWrites { + vk::WriteDescriptorSet { + .dstSet = descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }, + vk::WriteDescriptorSet { + .dstSet = descriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + } + }; + + device.updateDescriptorSets(descriptorWrites, {}); + } + #ifdef DEBUG + std::cout << "Descriptor sets created !\n"; + #endif + } + + void createCommandBuffers() { + #ifdef DEBUG + std::cout << "Creating a command buffers\n"; + #endif + + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = MAX_FRAMES_IN_FLIGHT + }; + commandBuffers = vk::raii::CommandBuffers(device, allocInfo); + + #ifdef DEBUG + std::cout << "Command buffer created\n"; + #endif + } + + void recordCommandBuffer(uint32_t imageIndex) { + auto &commandBuffer = commandBuffers[frameIndex]; + commandBuffer.begin( {} ); + + transition_image_layout( + swapChainImages[frameIndex], + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::ImageAspectFlagBits::eColor + ); + + transition_image_layout( + *depthImage, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eDepthAttachmentOptimal, + vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, + vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests, + vk::ImageAspectFlagBits::eDepth + ); + + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + vk::RenderingAttachmentInfo attachementInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { + .offset = { 0, 0 }, + .extent = swapChainExtent + }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &attachementInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + commandBuffer.beginRendering(renderingInfo); + commandBuffer.bindPipeline( vk::PipelineBindPoint::eGraphics, graphicsPipeline ); + commandBuffer.bindVertexBuffers(0, *vertexBuffer, {0}); + commandBuffer.bindIndexBuffer( *indexBuffer, 0, vk::IndexType::eUint16 ); + // Dynamic ViewPort + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0,0), swapChainExtent)); + // The actual rendering + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + *descriptorSets[frameIndex], + nullptr + ); + commandBuffer.drawIndexed( indices.size(), 1, 0, 0, 0); + commandBuffer.endRendering(); + transition_image_layout( + swapChainImages[imageIndex], + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe, + vk::ImageAspectFlagBits::eColor + ); + + commandBuffer.end(); + } + + void transition_image_layout ( + vk::Image image, + vk::ImageLayout oldLayout, + vk::ImageLayout newLayout, + vk::AccessFlags2 srcAccessMask, + vk::AccessFlags2 dstAccessMask, + vk::PipelineStageFlags2 srcStageMask, + vk::PipelineStageFlags2 dstStageMask, + vk::ImageAspectFlags image_aspect_flags + ){ + 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 = image, + .subresourceRange = { + .aspectMask = image_aspect_flags, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + vk::DependencyInfo dependencyInfo = { + .dependencyFlags = {}, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &barrier + }; + + commandBuffers[frameIndex].pipelineBarrier2(dependencyInfo); + } + + // Warning, the above function and the following have the same name, but not the same purpose, + // one is for the Pipeline (image displayed) and the other is for the texture image. + + void transitionImageLayout( + const vk::raii::Image& image, + vk::ImageLayout oldLayout, + vk::ImageLayout newLayout + ) { + auto commandBuffer = beginSingleTimeCommands(); + + vk::PipelineStageFlags sourceStage; + vk::PipelineStageFlags destinationStage; + + vk::ImageMemoryBarrier barrier { + .oldLayout = oldLayout, + .newLayout = newLayout, + .image = image, + .subresourceRange = { + vk::ImageAspectFlagBits::eColor, + 0, + 1, + 0, + 1 + } + }; + + if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) { + barrier.srcAccessMask = {}; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; + + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; + destinationStage = vk::PipelineStageFlagBits::eTransfer; + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + + sourceStage = vk::PipelineStageFlagBits::eTransfer; + destinationStage = vk::PipelineStageFlagBits::eFragmentShader; + } else { + throw std::invalid_argument("Unsupported layout transition!"); + } + + commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, nullptr, barrier); + + endSingleTimeCommands(commandBuffer); + } + + void copyBufferToImage( + const vk::raii::Buffer &buffer, + vk::raii::Image &image, + uint32_t width, + uint32_t height + ) { + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + + vk::BufferImageCopy region{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = { + vk::ImageAspectFlagBits::eColor, + 0, + 0, + 1 + }, + .imageOffset = {0, 0, 0}, + .imageExtent = {width, height, 1} + }; + commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, {region}); + endSingleTimeCommands(commandBuffer); + } + + void updateUniformBuffer(uint32_t currentImage) { + static auto startTime = std::chrono::high_resolution_clock::now(); + + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); + + UniformBufferObject ubo {}; + ubo.model = rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.view = lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.proj = glm::perspective(glm::radians(45.0f), static_cast(swapChainExtent.width) / static_cast(swapChainExtent.height), 0.1f, 10.0f); + ubo.proj[1][1] *= -1; + + memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + } + + void drawFrame(){ + auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX); + if (fenceResult != vk::Result::eSuccess) { + throw std::runtime_error("Failed to wait for fence !\nAborting..."); + } + device.resetFences(*inFlightFences[frameIndex]); + + auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr); + + if (result == vk::Result::eErrorOutOfDateKHR) { + #ifdef DEBUG + std::cout << "SwapChain out of date ! \n"; + #endif + recreateSwapChain(); + #ifdef DEBUG + std::cout << "Fixed the swapchain !\n"; + #endif + return; + } else if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) { + assert(result == vk::Result::eTimeout || result == vk::Result::eNotReady); + throw std::runtime_error("Failed to acquire SwapChain image !\nAborting..."); + } + + device.resetFences(*inFlightFences[frameIndex]); + + commandBuffers[frameIndex].reset(); + recordCommandBuffer(imageIndex); + + vk::PipelineStageFlags waitDestinationStageMask( vk::PipelineStageFlagBits::eColorAttachmentOutput ); + const vk::SubmitInfo submitInfo { + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*presentCompleteSemaphores[frameIndex], + .pWaitDstStageMask = &waitDestinationStageMask, + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffers[frameIndex], + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*renderFinishedSemaphores[imageIndex] + }; + updateUniformBuffer(frameIndex); + queue.submit(submitInfo, *inFlightFences[frameIndex]); + + const vk::PresentInfoKHR presentInfoKHR { + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex + }; + + result = queue.presentKHR(presentInfoKHR); + + if ((result == vk::Result::eSuboptimalKHR) || (result == vk::Result::eErrorOutOfDateKHR) || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); + } else { + assert(result == vk::Result::eSuccess); + } + + frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT; + } + + void createSyncObjects() { + assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty()); + + vk::SemaphoreCreateInfo semaphoreInfo = {}; + vk::FenceCreateInfo fenceInfo = { + .flags = vk::FenceCreateFlagBits::eSignaled + }; + + for (size_t i = 0; i < swapChainImages.size(); i++) { + renderFinishedSemaphores.emplace_back(device, semaphoreInfo); + } + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + presentCompleteSemaphores.emplace_back(device, semaphoreInfo); + inFlightFences.emplace_back(device, fenceInfo); + } + } + + void recreateSwapChain() { + #ifdef DEBUG + std::cout << "Re-creating the SwapChain...\n"; + #endif + + int width { 0 }; + int height { 0 }; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + device.waitIdle(); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createDepthResources(); + + #ifdef DEBUG + std::cout << "SwapChain re-created sucessfully !\n"; + #endif + } + + void createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties, + vk::raii::Buffer& buffer, + vk::raii::DeviceMemory& bufferMemory + ) { + #ifdef DEBUG + std::cout << "Creating a buffer of type: " << vk::to_string(usage) << '\n'; + #endif + + vk::BufferCreateInfo bufferInfo { + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + buffer = vk::raii::Buffer(device, bufferInfo); + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + vk::MemoryAllocateInfo allocInfo { + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + bufferMemory = vk::raii::DeviceMemory(device, allocInfo); + buffer.bindMemory(*bufferMemory, 0); + + #ifdef DEBUG + std::cout << "Buffer of type: " << vk::to_string(usage) << " sucessfully created !\n"; + #endif + } + + static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback( + vk::DebugUtilsMessageSeverityFlagBitsEXT severity, + vk::DebugUtilsMessageTypeFlagsEXT type, + const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData, + void * + ){ + if (severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError || severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) { + #ifdef DEBUG + std::cerr << "Validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl; + #endif + } + + return vk::False; + } + + static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; + } + + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + throw std::runtime_error("Failed to find a suitable memory type !\nAborting..."); + } + + std::vector getRequiredExtensions() { + uint32_t glfwExtensionCount = 0; + auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + if (enableValidationLayers) { + extensions.push_back(vk::EXTDebugUtilsExtensionName); + } + + return extensions; + } + + uint32_t findQueueFamilies(vk::raii::PhysicalDevice physicalDevice) { + #ifdef DEBUG + std::cout << "Selecting a queue...\n"; + #endif + // Get the index of the first queue that support graphics + std::vector queueFamilyProperties = physicalDevice.getQueueFamilyProperties(); + + for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) { + if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface)) { + // The queue has both graphics and present + #ifdef DEBUG + std::cout << "Found a graphics and present queue !\n"; + #endif + queueIndex = qfpIndex; + return static_cast(qfpIndex); + } + } + + //return static_cast( std::distance( queueFamilyProperties.begin(), queueFamilyProperty ) ); + throw std::runtime_error("Failed to find a queue with graphics and present !\n Aborting...\n"); + } + + vk::SurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { + return availableFormat; + } + } + return availableFormats[0]; + } + + vk::PresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) { + return availablePresentMode; + } + } + return vk::PresentModeKHR::eFifo; + } + + vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } + int width, height; + glfwGetFramebufferSize(window, &width, &height); + return { + std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width), + std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height) + }; + } + + static std::vector readFile(const std::string& filename) { + #ifdef DEBUG + std::cout << "Loading " << filename << " from disk...\n"; + #endif + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file !\n"); + } + + std::vector buffer(file.tellg()); + file.seekg(0, std::ios::beg); + file.read(buffer.data(), static_cast(buffer.size())); + file.close(); + #ifdef DEBUG + std::cout << "File loaded with size of " << buffer.size() << '\n'; + #endif + return buffer; + } + + [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector& code) const { + #ifdef DEBUG + std::cout << "Creating shader module\n"; + #endif + vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size() * sizeof(char), + .pCode = reinterpret_cast(code.data()) + }; + vk::raii::ShaderModule shaderModule { device, createInfo }; + #ifdef DEBUG + std::cout << "Shader module created\n"; + #endif + return shaderModule; + } + + vk::raii::CommandBuffer beginSingleTimeCommands() { + vk::CommandBufferAllocateInfo allocInfo { + .commandPool = commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffer commandBuffer = std::move(device.allocateCommandBuffers(allocInfo).front()); + + vk::CommandBufferBeginInfo beginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit }; + commandBuffer.begin(beginInfo); + + return commandBuffer; + } + + void endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer){ + commandBuffer.end(); + + vk::SubmitInfo submitInfo { + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + queue.submit(submitInfo, nullptr); + queue.waitIdle(); + } + + vk::raii::ImageView createImageView( + vk::Image image, + vk::Format format, + vk::ImageAspectFlags aspectFlags + ) { + vk::ImageViewCreateInfo viewInfo{ + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange = { + aspectFlags, + 0, + 1, + 0, + 1 + } + }; + + return vk::raii::ImageView( device, viewInfo ); + } + + vk::Format findSupportedFormat( + const std::vector& candidates, + vk::ImageTiling tiling, + vk::FormatFeatureFlags features + ){ + for (const auto format : candidates) { + vk::FormatProperties props = physicalDevice.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) { + return format; + } + if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("Failed to find supported format !"); + } + + vk::Format findDepthFormat() { + return findSupportedFormat( + { + vk::Format::eD32Sfloat, + vk::Format::eD32SfloatS8Uint, + vk::Format::eD24UnormS8Uint + }, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment + ); + } + + bool hasStensilComponent(vk::Format format) { + return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; + } + +}; + +int main() { + #ifdef DEBUG + std::cout << "Starting the app...\n"; + #endif + HelloTriangleApplication app; + + try { + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what(); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/textures/texture.jpg b/textures/texture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..975e3380e5a5068c05cb7dd1f168667bd9b69c0a GIT binary patch literal 77120 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

4a`?w~g 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