From add6b24fcec84fde98adabe7f39ebeda664d2b39 Mon Sep 17 00:00:00 2001 From: Iago Calvo Lista Date: Wed, 8 Jul 2026 14:26:43 +0100 Subject: [PATCH 1/3] Add SIGGRAPH2026 Vulkan tutorial --- .gitignore | 6 +- .../siggraph2026_vk_tutorial/.clang-format | 8 + .../siggraph2026_vk_tutorial/.gitignore | 9 + .../siggraph2026_vk_tutorial/.gitmodules | 29 + .../siggraph2026_vk_tutorial/CMakeLists.txt | 171 + .../CMakePresets.json | 54 + attachments/siggraph2026_vk_tutorial/LICENSE | 24 + .../siggraph2026_vk_tutorial/README.md | 135 + .../data/SponzaExtras/ABOUT.md | 19 + .../SponzaExtras/VulkanLogo/VulkanLogo.bin | Bin 0 -> 351524 bytes .../SponzaExtras/VulkanLogo/VulkanLogo.gltf | 137 + .../data/SponzaExtras/camera.gltf | 44 + .../init_submodules.py | 170 + .../shaders/.clang-format | 11 + .../shaders/albedo.frag.slang | 99 + .../shaders/basic.frag.slang | 107 + .../shaders/basic.vert.slang | 96 + .../shaders/solid_color.frag.slang | 60 + .../siggraph2026_vk_tutorial/src/main.cpp | 4339 +++++++++++++++++ .../siggraph2026_vk_tutorial/src/main.h | 576 +++ .../siggraph2026_vk_tutorial/src/util.cpp | 1471 ++++++ .../siggraph2026_vk_tutorial/src/util.h | 357 ++ .../siggraph2026_vk_tutorial/00_Overview.adoc | 6 +- 23 files changed, 7924 insertions(+), 4 deletions(-) create mode 100644 attachments/siggraph2026_vk_tutorial/.clang-format create mode 100644 attachments/siggraph2026_vk_tutorial/.gitignore create mode 100644 attachments/siggraph2026_vk_tutorial/.gitmodules create mode 100644 attachments/siggraph2026_vk_tutorial/CMakeLists.txt create mode 100644 attachments/siggraph2026_vk_tutorial/CMakePresets.json create mode 100644 attachments/siggraph2026_vk_tutorial/LICENSE create mode 100644 attachments/siggraph2026_vk_tutorial/README.md create mode 100644 attachments/siggraph2026_vk_tutorial/data/SponzaExtras/ABOUT.md create mode 100644 attachments/siggraph2026_vk_tutorial/data/SponzaExtras/VulkanLogo/VulkanLogo.bin create mode 100644 attachments/siggraph2026_vk_tutorial/data/SponzaExtras/VulkanLogo/VulkanLogo.gltf create mode 100644 attachments/siggraph2026_vk_tutorial/data/SponzaExtras/camera.gltf create mode 100644 attachments/siggraph2026_vk_tutorial/init_submodules.py create mode 100644 attachments/siggraph2026_vk_tutorial/shaders/.clang-format create mode 100644 attachments/siggraph2026_vk_tutorial/shaders/albedo.frag.slang create mode 100644 attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang create mode 100644 attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang create mode 100644 attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang create mode 100644 attachments/siggraph2026_vk_tutorial/src/main.cpp create mode 100644 attachments/siggraph2026_vk_tutorial/src/main.h create mode 100644 attachments/siggraph2026_vk_tutorial/src/util.cpp create mode 100644 attachments/siggraph2026_vk_tutorial/src/util.h diff --git a/.gitignore b/.gitignore index 3bdf1b62..6feb509a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,8 @@ attachments/simple_engine/android/gradle/wrapper/** attachments/simple_engine/android/gradlew attachments/simple_engine/android/gradlew.bat -attachments/template/build/** \ No newline at end of file +attachments/template/build/** + +attachments/siggraph2026_vk_tutorial/.git/ +attachments/siggraph2026_vk_tutorial/external/ +attachments/siggraph2026_vk_tutorial/data/external diff --git a/attachments/siggraph2026_vk_tutorial/.clang-format b/attachments/siggraph2026_vk_tutorial/.clang-format new file mode 100644 index 00000000..6dddf4e0 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/.clang-format @@ -0,0 +1,8 @@ +BasedOnStyle: LLVM +Language: Cpp +ColumnLimit: 120 +IndentWidth: 4 +AccessModifierOffset: -4 +BreakBeforeBraces: Stroustrup +PointerAlignment: Left +SortIncludes: CaseSensitive diff --git a/attachments/siggraph2026_vk_tutorial/.gitignore b/attachments/siggraph2026_vk_tutorial/.gitignore new file mode 100644 index 00000000..30f25233 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/.gitignore @@ -0,0 +1,9 @@ +build/ +out/ +.vs/ +.cache/ +.vscode/ +__pycache__/ +CMakeUserPresets.json +compile_commands.json +imgui.ini diff --git a/attachments/siggraph2026_vk_tutorial/.gitmodules b/attachments/siggraph2026_vk_tutorial/.gitmodules new file mode 100644 index 00000000..c086db7d --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/.gitmodules @@ -0,0 +1,29 @@ +[submodule "glfw"] + path = external/glfw + url = https://github.com/glfw/glfw.git + commit = b00e6a8a88ad1b60c0a045e696301deb92c9a13e +[submodule "glm"] + path = external/glm + url = https://github.com/g-truc/glm.git + commit = 6f14f4792a0cde5d0cf2c910506724d61cb95834 +[submodule "vk-bootstrap"] + path = external/vk-bootstrap + url = https://github.com/charles-lunarg/vk-bootstrap.git + commit = 5ca6780498864ae4c12f3a594ee6a6c5133d4ce0 +[submodule "nlohmann"] + path = external/nlohmann + url = https://github.com/nlohmann/json.git + commit = 4fad4468974a7b1b26d374b1c5955d2ac7d449b0 +[submodule "stb"] + path = external/stb + url = https://github.com/nothings/stb.git + commit = 31c1ad37456438565541f4919958214b6e762fb4 +[submodule "tinygltf"] + path = external/tinygltf + url = https://github.com/syoyo/tinygltf.git + commit = d31c16e333a6c8d593cad43f325f4e1825dd4776 +[submodule "gltf-sample-assets"] + path = data/external/sponza + url = https://github.com/KhronosGroup/glTF-Sample-Assets.git + commit = 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf + sparse-path = Models/Sponza diff --git a/attachments/siggraph2026_vk_tutorial/CMakeLists.txt b/attachments/siggraph2026_vk_tutorial/CMakeLists.txt new file mode 100644 index 00000000..7b531a41 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/CMakeLists.txt @@ -0,0 +1,171 @@ +cmake_minimum_required(VERSION 3.26) + +# This tutorial builds one executable that renders a descriptor-heap glTF scene with Vulkan shader objects. +project(vulkan_siggraph LANGUAGES CXX) + +# Export compile_commands.json so editors and language servers can understand the C++ build. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL "Export compile_commands.json for language servers") + +# The Vulkan SDK provides headers, the loader library, and the Slang compiler used below. +find_package(Vulkan REQUIRED) + +# Keep third-party libraries small: this sample only needs their runtime/library targets. +set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GLFW_INSTALL OFF CACHE BOOL "" FORCE) +set(GLM_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(VK_BOOTSTRAP_TEST OFF CACHE BOOL "" FORCE) +set(VK_BOOTSTRAP_WERROR OFF CACHE BOOL "" FORCE) + +# Add vendored dependencies. vk-bootstrap removes a lot of boilerplate from instance, +# device, queue, and swapchain creation, which keeps the tutorial focused on Vulkan flow. +add_subdirectory(external/glfw) +add_subdirectory(external/glm) +add_subdirectory(external/vk-bootstrap) + +# Slang compiles the small tutorial shaders to SPIR-V, the binary shader format Vulkan consumes. +find_program(SLANGC_EXECUTABLE + NAMES slangc + HINTS "$ENV{VULKAN_SDK}/Bin" + DOC "Path to the Slang compiler executable" +) +if(NOT SLANGC_EXECUTABLE) + message(FATAL_ERROR "slangc was not found. Configure with -DSLANGC_EXECUTABLE= or add slangc to PATH.") +endif() + +# CMake writes compiled shader outputs into the build tree, not the source tree. +set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") + +# Helper for declaring one shader compile step. The executable depends on the custom +# targets created here, so shaders are compiled before the app is linked/run. +function(add_slang_shader target_name source_file entry_point stage output_file) + set(reflection_json "${output_file}.json") + + # Store shader source files so they can be added to the project + set_source_files_properties("${source_file}" PROPERTIES HEADER_FILE_ONLY TRUE) + set_property(GLOBAL APPEND PROPERTY SIGGRAPH_SHADER_SOURCES "${source_file}") + + add_custom_command( + OUTPUT "${output_file}" "${reflection_json}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${SHADER_OUTPUT_DIR}" + COMMAND "${SLANGC_EXECUTABLE}" + "${source_file}" + -entry "${entry_point}" # Main function, for example vertexMain or fragmentMain. + -stage "${stage}" # vertex, fragment, etc. + -target spirv + -profile spirv_1_5 + -matrix-layout-column-major # Keep Slang's matrix layout explicit. + -reflection-json "${reflection_json}" # Generate reflection data as JSON. + -o "${output_file}" + DEPENDS "${source_file}" + VERBATIM + COMMENT "Compiling ${source_file}" + ) + add_custom_target("${target_name}" + DEPENDS "${output_file}" "${reflection_json}" + SOURCES "${source_file}" + ) +endfunction() + +# Output filenames used by the C++ code through SIGGRAPH_SHADER_DIR. +set(BASIC_VERT_SPV "${SHADER_OUTPUT_DIR}/basic.vert.spv") +set(BASIC_FRAG_SPV "${SHADER_OUTPUT_DIR}/basic.frag.spv") +set(ALBEDO_FRAG_SPV "${SHADER_OUTPUT_DIR}/albedo.frag.spv") +set(SOLID_COLOR_FRAG_SPV "${SHADER_OUTPUT_DIR}/solid_color.frag.spv") + +# Compile the vertex shader. It transforms scene vertices and reads per-object transforms through descriptor heap mappings. +add_slang_shader( + basic_vertex_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/basic.vert.slang" + vertexMain + vertex + "${BASIC_VERT_SPV}" +) + +# Compile the fragment shader. It samples textures selected through a push-index heap mapping. +add_slang_shader( + basic_fragment_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/basic.frag.slang" + fragmentMain + fragment + "${BASIC_FRAG_SPV}" +) + +# Compile the albedo-only fragment shader. It samples albedo and lights with interpolated geometry normals. +add_slang_shader( + albedo_fragment_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/albedo.frag.slang" + fragmentMain + fragment + "${ALBEDO_FRAG_SPV}" +) + +# Compile the solid-color fragment shader. It reads a constant-offset uniform buffer descriptor. +add_slang_shader( + solid_color_fragment_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/solid_color.frag.slang" + fragmentMain + fragment + "${SOLID_COLOR_FRAG_SPV}" +) + +# Main tutorial executable sources. +add_executable(vulkan_siggraph + src/main.cpp + src/main.h + src/util.cpp + src/util.h +) + +# Save shaders in their own source group called shaders +# Visual Studio will display this as a folder: vulkan_siggraph/shaders +get_property(SIGGRAPH_SHADER_SOURCES GLOBAL PROPERTY SIGGRAPH_SHADER_SOURCES) +target_sources(vulkan_siggraph PRIVATE ${SIGGRAPH_SHADER_SOURCES}) +source_group("Shaders" FILES ${SIGGRAPH_SHADER_SOURCES}) + +# Make shader compilation part of the normal application build. +add_dependencies(vulkan_siggraph basic_vertex_shader basic_fragment_shader albedo_fragment_shader solid_color_fragment_shader) + +# SIGGRAPH_SHADER_DIR tells the app where CMake put the SPIR-V files. +# SIGGRAPH_DATA_DIR points at the sample assets used by the descriptor heap tutorial. +# The Vulkan-Hpp definitions enable dynamic dispatch loading and designated initializers. +target_compile_definitions(vulkan_siggraph + PRIVATE + GLFW_INCLUDE_NONE + SIGGRAPH_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" + SIGGRAPH_SHADER_DIR="${SHADER_OUTPUT_DIR}" + VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 + VULKAN_HPP_NO_STRUCT_CONSTRUCTORS +) + +# Allow source files to include local headers with #include "util.h". +target_include_directories(vulkan_siggraph PRIVATE src) +target_include_directories(vulkan_siggraph SYSTEM PRIVATE external/nlohmann/single_include) +target_include_directories(vulkan_siggraph SYSTEM PRIVATE external/stb) +target_include_directories(vulkan_siggraph SYSTEM PRIVATE external/tinygltf) + +# The sample uses C++23 library/features and Vulkan-Hpp designated initialization. +target_compile_features(vulkan_siggraph PRIVATE cxx_std_23) + +# Link Vulkan, the window library, math helpers and bootstrap helpers +target_link_libraries(vulkan_siggraph + PRIVATE + Vulkan::Vulkan + glfw + glm::glm + vk-bootstrap::vk-bootstrap +) + +# Use strict warnings so tutorial code stays clear and portable across compilers. +if(MSVC) + target_compile_options(vulkan_siggraph PRIVATE /std:c++latest /W4 /permissive-) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(vulkan_siggraph PRIVATE /clang:-Wno-missing-designated-field-initializers) + endif() +else() + target_compile_options(vulkan_siggraph PRIVATE -std=c++2c -Wall -Wextra -Wpedantic -Wno-missing-designated-field-initializers) +endif() + +# In Visual Studio, make the tutorial executable the default startup project. +set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY VS_STARTUP_PROJECT vulkan_siggraph) diff --git a/attachments/siggraph2026_vk_tutorial/CMakePresets.json b/attachments/siggraph2026_vk_tutorial/CMakePresets.json new file mode 100644 index 00000000..2a65631c --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/CMakePresets.json @@ -0,0 +1,54 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "clang-debug", + "displayName": "Clang Debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/clang", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "clang-cl-debug", + "displayName": "Clang-CL Debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/clang-cl", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang-cl", + "CMAKE_CXX_COMPILER": "clang-cl", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "vs2022-debug", + "displayName": "Visual Studio 2022 x64 Debug", + "generator": "Visual Studio 17 2022", + "binaryDir": "${sourceDir}/build/vs2022", + "architecture": { + "value": "x64", + "strategy": "set" + } + } + ], + "buildPresets": [ + { + "name": "clang-debug", + "configurePreset": "clang-debug" + }, + { + "name": "clang-cl-debug", + "configurePreset": "clang-cl-debug" + }, + { + "name": "vs2022-debug", + "configurePreset": "vs2022-debug", + "configuration": "Debug" + } + ] +} diff --git a/attachments/siggraph2026_vk_tutorial/LICENSE b/attachments/siggraph2026_vk_tutorial/LICENSE new file mode 100644 index 00000000..614c19e3 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 Khronos Group + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/attachments/siggraph2026_vk_tutorial/README.md b/attachments/siggraph2026_vk_tutorial/README.md new file mode 100644 index 00000000..42b86050 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/README.md @@ -0,0 +1,135 @@ + +# SIGGRAPH 2026: How to write a Vulkan application + +This is a simple tutorial with the objective of explaining how to write a Vulkan application in 2026. +The tutorial employs the latest recommended best practices for beginners: + +- Descriptor heap +- Shader Object +- Dynamic Rendering + +Shaders are written using Slang, a modern shading language hosted by the Khronos Group. + +See the [course page](https://github.com/KhronosGroup/Vulkan-Tutorial/blob/main/en/courses/siggraph2026_vk_tutorial) for more information. + +> [!NOTE] +> ChatGPT and Codex were used to generate code for this tutorial. +> However, all code has been reviewed, edited, and analyzed. + +## Build + +### Prerequisites + +- Visual Studio 2022 or Clang and Ninja +- CMake 3.26 or newer +- Vulkan SDK +- `slangc`, either on `PATH` or supplied with `-DSLANGC_EXECUTABLE=...` + +> [!NOTE] +> The tutorial builds and runs on Windows and Linux. Android is not supported. +> This tutorial requires a lot of modern Vulkan extensions, including `VK_EXT_descriptor_heap` and `VK_EXT_shader_object`. +> Old Vulkan drivers will not be able to run this tutorial. + +### Setup + +After cloning the repository, initialize the pinned dependencies with: + +```bash +python .\init_submodules.py +``` + +The `commit = ...` entries in `.gitmodules` are a project convention used by +`init_submodules.py` to pin each dependency. Plain Git ignores this field. +The Sponza asset submodule also uses `sparse-path = Models/Sponza` so the setup +script checks out only the Sponza folder from `glTF-Sample-Assets`. + +If you need to set up the dependencies manually, run the matching commands for +each dependency: + +```bash +git init + +git submodule add --force --name glfw https://github.com/glfw/glfw.git external/glfw +git submodule add --force --name glm https://github.com/g-truc/glm.git external/glm +git submodule add --force --name vk-bootstrap https://github.com/charles-lunarg/vk-bootstrap.git external/vk-bootstrap +git submodule add --force --name nlohmann https://github.com/nlohmann/json.git external/nlohmann +git submodule add --force --name stb https://github.com/nothings/stb.git external/stb +git submodule add --force --name tinygltf https://github.com/syoyo/tinygltf.git external/tinygltf + +git clone --filter=blob:none --sparse --no-checkout https://github.com/KhronosGroup/glTF-Sample-Assets.git data/external/sponza +git -C data/external/sponza sparse-checkout set Models/Sponza +git -C data/external/sponza checkout 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf +git submodule add --force --name gltf-sample-assets https://github.com/KhronosGroup/glTF-Sample-Assets.git data/external/sponza +git submodule absorbgitdirs data/external/sponza + +git submodule init +git submodule update --init --recursive + +git -C external/glfw checkout b00e6a8a88ad1b60c0a045e696301deb92c9a13e +git -C external/glm checkout 6f14f4792a0cde5d0cf2c910506724d61cb95834 +git -C external/vk-bootstrap checkout 5ca6780498864ae4c12f3a594ee6a6c5133d4ce0 +git -C external/nlohmann checkout 4fad4468974a7b1b26d374b1c5955d2ac7d449b0 +git -C external/stb checkout 31c1ad37456438565541f4919958214b6e762fb4 +git -C external/tinygltf checkout d31c16e333a6c8d593cad43f325f4e1825dd4776 +git -C data/external/sponza sparse-checkout set Models/Sponza +git -C data/external/sponza checkout 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf +``` + +### Compile with Visual Studio (Windows) + +The repo includes a Visual Studio 2022 x64 preset: + +```bash +cmake --preset vs2022-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" +``` + +This writes Visual Studio project files and the executable under `build/vs2022`. + +To build: +```bash +cmake --build --preset vs2022-debug +``` + +### Compile with Clang (Windows and Linux) + +The repo includes CMake presets for this workflow: + +```bash +cmake --preset clang-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" +cmake --build --preset clang-debug +``` + +> [!NOTE] +> `-DSLANGC_EXECUTABLE` is optional. If not provided, CMake will use the version of `slangc` in `PATH`. + +## Run + +```bash +.\build\vs2022\Debug\vulkan_siggraph.exe +``` + +## Files + +- `src/main.cpp`, `src/main.h`: Main application containing the Vulkan code. +- `src/util.cpp`, `src/util.h`: Collection of helper functions to make the tutorial easy to follow. Functions in these files parse reflection data, glTF objects, textures, and related inputs, so they are less relevant if your objective is to learn Vulkan. +- `shaders/`: Slang shaders. + +## License + +This SIGGRAPH tutorial sample is licensed under the [MIT License](LICENSE). +This license applies to the sample code and documentation in this directory. +Assets and third-party libraries remain under their own licenses. + +## Assets + +- **Sponza** - Cryengine Limited License Agreement (`LicenseRef-CRYENGINE-Agreement`). Building interior glTF scene used for lighting and rendering tests. See [glTF Sample Assets](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/Sponza). +- **Vulkan logo** - Khronos trademark. The Vulkan logo is a trademark of The Khronos Group Inc. The MIT license does not grant trademark rights. See the [Khronos trademark guidelines](https://www.khronos.org/legal/trademarks/). + +## Third-party Libraries + +- **GLFW** - [glfw/glfw](https://github.com/glfw/glfw): Window creation and Vulkan surface integration ([zlib License](https://github.com/glfw/glfw/blob/master/LICENSE.md)). +- **GLM** - [g-truc/glm](https://github.com/g-truc/glm): Header-only math library for vectors, matrices, and transforms ([MIT or Happy Bunny License](https://github.com/g-truc/glm/blob/master/copying.txt)). +- **vk-bootstrap** - [charles-lunarg/vk-bootstrap](https://github.com/charles-lunarg/vk-bootstrap): Helper library for Vulkan instance, device, queue, and swapchain setup ([MIT License](https://github.com/charles-lunarg/vk-bootstrap/blob/main/LICENSE.txt)). +- **nlohmann/json** - [nlohmann/json](https://github.com/nlohmann/json): Header-only JSON parser ([MIT License](https://github.com/nlohmann/json/blob/develop/LICENSE.MIT)). +- **stb** - [nothings/stb](https://github.com/nothings/stb): Header-only utility library; this tutorial uses `stb_image` for texture loading ([MIT or Public Domain / Unlicense](https://github.com/nothings/stb/blob/master/LICENSE)). +- **tinygltf** - [syoyo/tinygltf](https://github.com/syoyo/tinygltf): Header-only glTF 2.0 loader ([MIT License](https://github.com/syoyo/tinygltf/blob/release/LICENSE)). diff --git a/attachments/siggraph2026_vk_tutorial/data/SponzaExtras/ABOUT.md b/attachments/siggraph2026_vk_tutorial/data/SponzaExtras/ABOUT.md new file mode 100644 index 00000000..a3d0e9cd --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/data/SponzaExtras/ABOUT.md @@ -0,0 +1,19 @@ +# Assets + +The repository contains the following assets: +- **data/external/sponza/Models/Sponza** - A common building interior scene used for showcasing rendering techniques. Using the version from the [Khronos Group glTF Sample Assets](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/Sponza) repository, licensed under the [Cryengine Limited License Agreement](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/Sponza/LICENSE.md). +- **data/SponzaExtras/VulkanLogo** - Scene using the 3D Version of the Vulkan logo. +- **data/SponzaExtras/camera.gltf** - Contains a camera to visualize Sponza. + +All glTFs are designed to be used together. Camera and VulkanLogo are scaled and placed according to the version of Sponza. + +Vulkan and the Vulkan logo are registered trademarks of the Khronos Group Inc. +https://www.khronos.org/legal/trademarks/ + + +This files are designed for use in the SIGGRAPH 2026 course: How to write a Vulkan application in 2026. + +The camera glTF contains a camera to visualize Sponza. + + + diff --git a/attachments/siggraph2026_vk_tutorial/data/SponzaExtras/VulkanLogo/VulkanLogo.bin b/attachments/siggraph2026_vk_tutorial/data/SponzaExtras/VulkanLogo/VulkanLogo.bin new file mode 100644 index 0000000000000000000000000000000000000000..b2816f191a727245bc5dc781e45a829dbd1a1c35 GIT binary patch literal 351524 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

Yf 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

{XzRU4VH{hyFFJl${8djSV+G-q~E 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 g_camera; + +[[vk::binding(2, 0)]] +ConstantBuffer g_pointLight; + +[[vk::binding(3, 0)]] +Sampler2D g_albedoTexture; + +// In Slang we can use HLSL annotations to communicate attributes between vertex and fragment shaders. +struct FragmentInput { + float3 worldPosition : TEXCOORD2; + float3 normal : NORMAL; + float3 tangent : TANGENT; + float tangentSign : TEXCOORD1; + float2 uv : TEXCOORD0; +}; + +// Simple illumination function +float3 illuminate(float3 albedo, float3 worldPosition, float3 normal, LightData pointLight) +{ + // Basic Blinn-Phong lighting with distance attenuation. + float3 lightDelta = pointLight.pointPosition - worldPosition; + float lightDistance = length(lightDelta); + float3 lightDirection = lightDelta / max(lightDistance, 0.001); + float attenuation = 1.0 / (1.0 + 0.09 * lightDistance + 0.032 * lightDistance * lightDistance); + + float diffuseAmount = max(dot(normal, lightDirection), 0.0); + float3 viewDirection = normalize(g_camera.cameraPosition.xyz - worldPosition); + float3 halfVector = normalize(lightDirection + viewDirection); + float specularAmount = pow(max(dot(normal, halfVector), 0.0), 32.0) * 0.2; + + float3 ambient = albedo * pointLight.ambientColor * pointLight.ambientIntensity; + float3 direct = + (albedo * diffuseAmount + specularAmount) * pointLight.ambientColor * pointLight.pointIntensity * attenuation; + + return ambient + direct; +} + +[shader("fragment")] +float4 fragmentMain(FragmentInput input) : SV_Target +{ + // The albedo combined sampled image is selected by HEAP_WITH_PUSH_INDEX. + float4 texel = g_albedoTexture.Sample(input.uv); + if (texel.a < 0.5) { + // Discard transparent fragments. + discard; + } + + float3 normal = normalize(input.normal); + if (texel.a < 0.0) { + // HACK: Disable performance warning: + // [VK_SHADER_STAGE_VERTEX_BIT] has an Output value declared but there is no corresponding Input declared in + // [VK_SHADER_STAGE_FRAGMENT_BIT]. This is not invalid, but the write to the unused Output is discarded, causing + // a performance validation message. Proper solution is to have a custom vertex shader, but this is + // intentionally avoided to keep the tutorial simple. + normal += input.tangent * 0.001 + input.tangentSign * 0.001; + } + return float4(illuminate(texel.rgb, input.worldPosition, normalize(normal), g_pointLight), 1.0); +} diff --git a/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang b/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang new file mode 100644 index 00000000..6eabcc82 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang @@ -0,0 +1,107 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +struct CameraData { + float4x4 viewProjection; + float4 cameraPosition; +}; + +struct LightData { + float3 pointPosition; + float pointIntensity; + float3 ambientColor; + float ambientIntensity; +}; + +// Shared binding table: +// 0 camera. +// 1 object data. +// 2 point light. +// 3 albedo combined image sampler. +// 4 normal combined image sampler. +[[vk::binding(0, 0)]] +ConstantBuffer g_camera; + +[[vk::binding(2, 0)]] +ConstantBuffer g_pointLight; + +[[vk::binding(3, 0)]] +Sampler2D g_albedoTexture; + +[[vk::binding(4, 0)]] +Sampler2D g_normalTexture; + +// In Slang we can use HLSL annotations to communicate attributes between vertex and fragment shaders. +struct FragmentInput { + float3 worldPosition : TEXCOORD2; + float3 normal : NORMAL; + float3 tangent : TANGENT; + float tangentSign : TEXCOORD1; + float2 uv : TEXCOORD0; +}; + +// Transforms geometry normal using a normal texture +// Normal maps or textures allow us to simulate extra detail while keeping geometry low. +float3 CalculateNormalMap(FragmentInput input, Sampler2D normalTexture) +{ + float3 normal = normalize(input.normal); + float3 tangent = normalize(input.tangent - normal * dot(normal, input.tangent)); + float3 bitangent = normalize(cross(normal, tangent) * input.tangentSign); + float3 tangentSpaceNormal = normalTexture.Sample(input.uv).xyz * 2.0 - 1.0; + + return normalize(tangent * tangentSpaceNormal.x + bitangent * tangentSpaceNormal.y + normal * tangentSpaceNormal.z); +} + +// Simple illumination function +float3 illuminate(float3 albedo, float3 worldPosition, float3 normal, LightData pointLight) +{ + // Basic Blinn-Phong lighting with distance attenuation. + float3 lightDelta = pointLight.pointPosition - worldPosition; + float lightDistance = length(lightDelta); + float3 lightDirection = lightDelta / max(lightDistance, 0.001); + float attenuation = 1.0 / (1.0 + 0.09 * lightDistance + 0.032 * lightDistance * lightDistance); + + float diffuseAmount = max(dot(normal, lightDirection), 0.0); + float3 viewDirection = normalize(g_camera.cameraPosition.xyz - worldPosition); + float3 halfVector = normalize(lightDirection + viewDirection); + float specularAmount = pow(max(dot(normal, halfVector), 0.0), 32.0) * 0.2; + + float3 ambient = albedo * pointLight.ambientColor * pointLight.ambientIntensity; + float3 direct = + (albedo * diffuseAmount + specularAmount) * pointLight.ambientColor * pointLight.pointIntensity * attenuation; + + return ambient + direct; +} + +[shader("fragment")] +float4 fragmentMain(FragmentInput input) : SV_Target +{ + // The albedo and normal combined sampled images are selected by HEAP_WITH_PUSH_INDEX. + float4 texel = g_albedoTexture.Sample(input.uv); + if (texel.a < 0.5) { + // Discard transparent fragments. + discard; + } + + float3 normal = CalculateNormalMap(input, g_normalTexture); + return float4(illuminate(texel.rgb, input.worldPosition, normal, g_pointLight), 1.0); +} diff --git a/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang b/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang new file mode 100644 index 00000000..8fba4a88 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang @@ -0,0 +1,96 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +struct CameraData { + float4x4 viewProjection; + float4 cameraPosition; +}; + +struct ObjectData { + float4x4 model; +}; +// +// Descriptor heap mapping binds this like a normal storage buffer in shader code, +// but the descriptor bytes come from the resource heap instead of a descriptor set. +// Shared binding table: +// 0 camera. +// 1 objects data. +// 2 point light. +// 3 albedo combined image sampler. +// 4 normal combined image sampler. +// +// [[vk::binding(bindingId, setId)]] is optional. It is used here for simplicity. +// Automatic deduction of bindingId and setId using Slang reflection might be preferred in most use cases. +[[vk::binding(0, 0)]] +ConstantBuffer g_camera; + +[[vk::binding(1, 0)]] +StructuredBuffer g_objects; + +// Vertex input layout for the basic vertex shader. +// This structure is filled by vkCmdBindVertexBuffers2. +// We use VertexInputAttributeDescription2EXT to set how to read the attributes from the buffer, +// linking the location in the shader to the binding and offset in the buffer. +// [[vk::location(locationId)]] is optional. It is used here for simplicity. +// Automatic deduction of locationId using Slang reflection might be preferred in most use cases. +struct VertexInput { + [[vk::location(0)]] + float3 position : POSITION; + [[vk::location(1)]] + float3 normal : NORMAL; + [[vk::location(2)]] + float4 tangent : TANGENT; + [[vk::location(3)]] + float2 uv : TEXCOORD0; +}; + +// Send data from vertex shader to fragment shader. +// Rasterization uses SV_Position while the remaining fields are interpolated for the fragment shader. +struct VertexOutput { + float4 position : SV_Position; + float3 worldPosition : TEXCOORD2; + float3 normal : NORMAL; + float3 tangent : TANGENT; + float tangentSign : TEXCOORD1; + float2 uv : TEXCOORD0; +}; + +[shader("vertex")] +VertexOutput vertexMain(VertexInput input, uint instanceIndex : SV_InstanceID, + uint baseInstance : SV_StartInstanceLocation) { + // SV_InstanceID is relative to firstInstance in SPIR-V generated by Slang. + // SV_StartInstanceLocation carries vkCmdDraw's firstInstance value, + // which this tutorial uses as the object index for one-instance draws. + ObjectData objectData = g_objects[baseInstance + instanceIndex]; + + VertexOutput output; + // Use the same matrix convention as the CPU upload path: + // projection * view * model * position. + float4 worldPosition = mul(objectData.model, float4(input.position, 1.0)); + output.position = mul(g_camera.viewProjection, worldPosition); + output.uv = input.uv; + output.normal = normalize(mul((float3x3)objectData.model, input.normal)); + output.tangent = normalize(mul((float3x3)objectData.model, input.tangent.xyz)); + output.tangentSign = input.tangent.w; + output.worldPosition = worldPosition.xyz; + return output; +} diff --git a/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang b/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang new file mode 100644 index 00000000..1e51f614 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang @@ -0,0 +1,60 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +struct SolidColorData { + float4 color; +}; + +// This shader intentionally reuses binding 2. +// Its shader-object mapping points that binding at a different descriptor heap offset +// than the textured shader's point light. +[[vk::binding(2, 0)]] +ConstantBuffer g_solidColor; + +struct FragmentInput { + float3 worldPosition : TEXCOORD2; + float3 normal : NORMAL; + float3 tangent : TANGENT; + float tangentSign : TEXCOORD1; + float2 uv : TEXCOORD0; +}; + +[shader("fragment")] +float4 fragmentMain(FragmentInput input) : SV_Target +{ + float4 color = g_solidColor.color; + + // HACK: Disable performance warning: + // [VK_SHADER_STAGE_VERTEX_BIT] has an Output value declared but there is no corresponding Input declared in + // [VK_SHADER_STAGE_FRAGMENT_BIT]. This is not invalid, but the write to the unused Output is discarded, causing a + // performance validation message. Proper solution is to have a custom vertex shader, but this is intentionally + // avoided to keep the tutorial simple. + if (color.a < 0.0f) { + color.xy += input.uv * 0.001; + color.w += input.tangentSign * 0.001; + color.xyz += input.tangent * 0.001; + color.xyz += input.normal * 0.001; + color.xyz += input.worldPosition * 0.001; + } + + return color; +} diff --git a/attachments/siggraph2026_vk_tutorial/src/main.cpp b/attachments/siggraph2026_vk_tutorial/src/main.cpp new file mode 100644 index 00000000..e53cb0ef --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/src/main.cpp @@ -0,0 +1,4339 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// The main objective of this course is to explain to developers how to use Vulkan. +// The current code is heavily documented, to explain most relevant Vulkan concepts. +// +// See the course page for more information. +// https://github.com/KhronosGroup/Vulkan-Tutorial/blob/main/en/courses/siggraph2026_vk_tutorial +// + +#include "main.h" + +#include +#include + +// Vulkan-Hpp loads extension functions through a dispatcher. +// This can create a dispatch table to cache function pointers, +// and can skip the loader when calling Vulkan functions. +VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE + +namespace siggraph { + +// Main entry point. +void Application::run() +{ + initGLFWWindow(); + + // Create the core Vulkan objects and the swapchain images we will render into. + initVulkanVKB(); + + // Initialize persistent Vulkan resources. + { + initCommandPool(); + initFramesInFlightResources(); + initSwapchainImageSyncObjects(); + + beginHelperCommands(); + initDepthImages(); + initSceneResources(); + endHelperCommandsAndFlushUploads(); + + initDescriptorHeaps(); + calculateVertexInputs(); + initShaderObjects(); + } + + // After initialization, we enter the main loop. + // The application will run generating frames until the user closes the window or we reach a frame limit. + mainLoop(); +} + +void Application::setFrameLimit(std::uint32_t frameLimit) { m_remainingFrameLimit = frameLimit; } + +//**************** +// Vulkan initialization +//**************** + +void Application::initGLFWWindow() +{ + util::log_msg("[Init] Init GLFW window"); + // GLFW handles cross-platform window creation for the tutorial. + // We could use SDL or platform-specific code instead. + util::require(glfwInit() == GLFW_TRUE, "Failed to initialize GLFW"); + + // Vulkan creates the rendering surface; GLFW should not create an OpenGL context. + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + // Disable window resizing to avoid recreation of swapchain and related resources. + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + m_window = glfwCreateWindow(windowWidth, windowHeight, "Vulkan SIGGRAPH 2026", nullptr, nullptr); + util::require(m_window != nullptr, "Failed to create GLFW window"); +} + +// Initialize Vulkan using vk-bootstrap. +// This creates the instance, surface, physical device, logical device, queues, and swapchain. +void Application::initVulkanVKB() +{ + util::log_msg("[Init] Init Vulkan using vk-bootstrap"); + + // vk-bootstrap reports failures as std::error_code values. + // This helper throws an exception with a descriptive message if the result is an error. + // If the object is created correctly it returns it + const auto vkbGetIfValid = [](auto&& result, const std::string_view operation) { + // Most Vulkan functions return a VkResult value that acts as an error code, indicating success or failure. + // vk-bootstrap wraps the error code and the actual object we created. + // vkbGetIfValid unwraps the result and checks if the object was created successfully. + + if (!result) { + std::string errorMessage = std::format("{} failed: {}", operation, result.error().message()); + for (const std::string& reason : result.detailed_failure_reasons()) { + errorMessage += "\n - " + reason; + } + throw std::runtime_error(errorMessage); + } + + return std::move(result).value(); + }; + + // Now we will initialize Vulkan using vk-bootstrap. + // In order to initialize Vulkan we need to create the Vulkan instance, select a physical device, + // create a logical device, create a swapchain and select queues to submit work. + // This is relatively easy to do but requires a lot of repetitive code. + // vk-bootstrap is a library that helps with Vulkan initialization by providing higher-level abstractions. + // It allows developers to skip writing the same initialization code and focus on creating the application. + // + // We recommend vk-bootstrap for small projects and beginners to simplify Vulkan initialization. + // vk-bootstrap allows people learning Vulkan to get a better idea of the relevant parts when developing Vulkan. + // Vulkan initialization is a verbose and repetitive process that can be easily learned later. + + // Create the Vulkan instance using vk-bootstrap. + { + + // This code creates a Vulkan instance using vk-bootstrap. + // The instance is the root Vulkan object. + // It represents the connection between the application and the Vulkan API and is used to create all other + // Vulkan objects. + + // Vulkan layers are optional components that can be enabled to provide additional functionality. + // They can hook Vulkan API calls and provide additional features like validation, debugging, and + // profiling. Vulkan layers are employed by tools like RenderDoc or gfxreconstruct to offer debugging and + // profiling capabilities for Vulkan applications. + // + // Vulkan validation layers are particularly important for beginners as they provide runtime validation of + // Vulkan API usage. They help developers catch bugs and incorrect usage of the Vulkan API. + // + // Vulkan layers can be enabled by the application, but we recommend using tools like vkconfig. + // [Vulkan Configurator](https://vulkan.lunarg.com/doc/view/latest/windows/vkconfig.html) + // This tool makes it easy to enable and configure layers. + // This is particularly useful for the validation layers, making it easier to enable additional validation + // checks like GPU-assisted validation or synchronization validation. + + // This tutorial uses vk-bootstrap to create the Vulkan instance and enable validation layers if available. + // Creating the instance in Vulkan is relatively easy. + // vk-bootstrap abstracts the process of enabling the validation layers and creating the debug messenger. + // In a normal app, we would first need to query all the available layers and check if the validation layers are + // available. Then we would build a vector of layers to enable and pass it to the instance creation info. + // A proper application should also use EXT_layer_settings to configure the validation layers and enable/disable + // specific checks. Vk-bootstrap significantly simplifies this process, + // checking for available layers and enabling the validation layers if requested. + // For layer configuration, we recommend using vkconfig. + + // Query global Vulkan support before requesting optional validation layers. + const vkb::SystemInfo systemInfo = + vkbGetIfValid(vkb::SystemInfo::get_system_info(), "Querying Vulkan system info"); + + // vk-bootstrap handles instance extension/layer selection and debug messenger setup. + auto instanceBuilder = + vkb::InstanceBuilder() + .set_app_name("Vulkan SIGGRAPH tutorial") + .set_engine_name("No Engine") // Vulkan allows us to set an engine name and application name. + .require_api_version(1, 4, 0); // Specify the minimum Vulkan API version. + + // Request validation layers. + if (debugData.enableGpuDebug) { + // vk-bootstrap abstracts enumerating and checking Vulkan validation layers. + // + // Note: When enabling use_default_debug_messenger(), vk-bootstrap requests the debug utils extension for + // debug names. + + util::require(systemInfo.validation_layers_available, + "GPU debug is enabled, but Vulkan validation layers are not available"); + + // We are also using a default debug messenger to handle validation messages. + // Vulkan allows us to write a specific callback to handle validation messages, + // but vk-bootstrap provides a default one that prints messages to the console. + instanceBuilder.request_validation_layers(true).use_default_debug_messenger(); + } + + // Ensure the instance was created successfully by vk-bootstrap. + m_vkbData.m_instance = vkbGetIfValid(instanceBuilder.build(), "Creating Vulkan instance"); + + // Load function pointers for the Vulkan instance. + { + // It is complicated to explain how Vulkan is loaded, but as an oversimplification: + // When calling a Vulkan function, the application does not call the function directly. + // First it calls the Vulkan loader, and the loader then obtains this function from the device or instance. + // Vulkan allows us to use vkGetInstanceProcAddr to cache the final function pointers, to call them directly + // and skip the lookup work done by the loader. + // + // The process of creating a table of function pointers is a bit tedious, so it is handled automatically by + // Vulkan-Hpp. Applications using the C API are encouraged to use a library like Volk for similar + // functionality. Most third-party bindings for other languages also handle this automatically. + // + // The VULKAN_HPP_DEFAULT_DISPATCHER contains the function pointers for the Vulkan instance and device + // functions. It is used by Vulkan-Hpp to call C API functions directly without going through the loader. + VULKAN_HPP_DEFAULT_DISPATCHER.init(m_vkbData.m_instance.instance, vkGetInstanceProcAddr); + } + } + + // Create the window surface using GLFW. + { + // We need to create the surface before physical-device selection so we can check presentation support + // when selecting the physical device. + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkResult result = glfwCreateWindowSurface(m_vkbData.m_instance.instance, m_window, nullptr, &surface); + util::checkVk(result, "Creating GLFW Vulkan surface"); + m_surface = vk::SurfaceKHR{surface}; + } + + // Select a physical device and create the logical device with required features and extensions. + { + + // In Vulkan, the machine issuing the Vulkan commands is the host, and the machine executing the commands is the + // device. Vulkan can run on multiple hardware configurations, but in general the device is the GPU, and the + // host is the CPU. Vulkan is designed to support systems with multiple GPUs, including laptops with integrated + // and dedicated graphics, or multi-GPU clusters. + // + // Vulkan differentiates between physical devices and logical devices. + // A physical device is a GPU that is available in the system, exposing the hardware capabilities. + // A logical device is a handle to a physical device. It specifies the capabilities enabled for our application + // and is used to issue commands. + // + // After creating the Vulkan instance, we need to select the device that we will use. + // This is abstracted by vk-bootstrap, but we can also do it manually by enumerating the available physical + // devices. Then we check which physical devices support the features and extensions we need, and select the + // best one for our application. Finally we create a logical device from the selected physical device, enabling + // the features and extensions we need. Checking capabilities for a physical device requires a lot of + // boilerplate code. First we need to obtain all extensions supported by the device, then we need to check if + // the extensions we want to use are supported. We need to build a pNext chain of feature structures for all the + // features and extensions we want to check. After querying the physical device properties and features, we need + // to build a new pNext chain only with the features and extensions we want to enable in the logical device. + // This is mostly boilerplate code that is abstracted by vk-bootstrap. + + // This tutorial uses a lot of modern extensions. + // We use this array to specify all the extensions we need in one place and pass it to vk-bootstrap when + // selecting the physical device. + const std::array requiredExtensions{ + VK_EXT_SHADER_OBJECT_EXTENSION_NAME, + VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME, + VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME, + VK_EXT_DESCRIPTOR_HEAP_EXTENSION_NAME, + }; + + // vk-bootstrap uses our instance to enumerate all physical devices and filter them based on the criteria we + // specify. + vkb::PhysicalDeviceSelector physicalDeviceSelector{m_vkbData.m_instance}; + + // Set all extensions and features required by the tutorial. + { + physicalDeviceSelector + .add_required_extensions(requiredExtensions.size(), requiredExtensions.data()) + // Check that the device supports presentation to our surface, so we can present rendered images. + .set_surface(static_cast(m_surface)) + // Vulkan 1.4 makes maintenance5 core; require the feature for bindIndexBuffer2. + // Descriptor heap also has a requirement on maintenance5. + .set_minimum_version(1, 4) + .add_required_extension_features(VkPhysicalDeviceVulkan14Features{ + // In the Vulkan C API, we need to specify the structure type in sType. + // Vulkan-Hpp handles this automatically, setting sType in the constructor of the structure, + // but we need to manually set sType when iterating with a C library like vk-bootstrap. + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES, + .maintenance5 = VK_TRUE, + }) + // Vulkan 1.3 core features used by the tutorial: synchronization2 and dynamic rendering. + .add_required_extension_features(VkPhysicalDeviceVulkan13Features{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, + .synchronization2 = VK_TRUE, + .dynamicRendering = VK_TRUE, + }) + // Buffer device address is core in Vulkan 1.2. + .add_required_extension_features(VkPhysicalDeviceVulkan12Features{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, + .bufferDeviceAddress = VK_TRUE, + }) + // Draw passes objectIndex as firstInstance. Slang exposes that value through + // SV_StartInstanceLocation so the shader can select one object transform per draw. + .add_required_extension_features(VkPhysicalDeviceVulkan11Features{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, + .shaderDrawParameters = VK_TRUE, + }) + // EXT_shader_object lets us bind shader objects directly instead of building a pipeline. + .add_required_extension_features(VkPhysicalDeviceShaderObjectFeaturesEXT{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_OBJECT_FEATURES_EXT, + .shaderObject = VK_TRUE, + }) + // These dynamic-state features allow the command buffer to set rasterization/blending state. + .add_required_extension_features(VkPhysicalDeviceExtendedDynamicState3FeaturesEXT{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT, + .extendedDynamicState3DepthClampEnable = VK_TRUE, + .extendedDynamicState3PolygonMode = VK_TRUE, + .extendedDynamicState3RasterizationSamples = VK_TRUE, + .extendedDynamicState3SampleMask = VK_TRUE, + .extendedDynamicState3AlphaToCoverageEnable = VK_TRUE, + .extendedDynamicState3AlphaToOneEnable = VK_TRUE, + .extendedDynamicState3LogicOpEnable = VK_TRUE, + .extendedDynamicState3ColorBlendEnable = VK_TRUE, + .extendedDynamicState3ColorWriteMask = VK_TRUE, + }) + .add_required_extension_features(VkPhysicalDeviceVertexInputDynamicStateFeaturesEXT{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_INPUT_DYNAMIC_STATE_FEATURES_EXT, + .vertexInputDynamicState = VK_TRUE, + }) + // This tutorial uses descriptor heaps to manage shader resources without pipeline layouts or descriptor + // sets. + .add_required_extension_features(VkPhysicalDeviceDescriptorHeapFeaturesEXT{ + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT, + .descriptorHeap = VK_TRUE, + }); + } + + std::optional selectedCandidate; + // Manually filter vk-bootstrap's candidates to select the best device/GPU possible. + { + // Get devices that meet criteria specified above; vk-bootstrap checks for required features and extensions. + const std::vector physicalDeviceCandidates = + vkbGetIfValid(physicalDeviceSelector.select_devices(), "Selecting Vulkan physical devices"); + + // We iterate through the candidates and select the best one based on additional criteria. + for (const vkb::PhysicalDevice& vkbPhysicalDeviceCandidate : physicalDeviceCandidates) { + bool valid = true; + // Create a Vulkan-Hpp wrapper around the raw C handle. + vk::PhysicalDevice physicalDeviceCandidate{vkbPhysicalDeviceCandidate.physical_device}; + + // Vk-bootstrap can return multiple candidates if multiple GPUs support the required features and + // extensions. This is an example of how to use some properties to further filter GPUs. + // This is a tutorial, but the best solution might be to sort GPU candidates based on a score calculated + // from their properties and features, and select the best one. + // It is good practice to offer configuration options to select a specific GPU if multiple are + // available. + // + // Format properties are not device features, so we need to filter vk-bootstrap's candidates manually. + // Check that the device supports linear blitting for the texture format used in the tutorial. + { + const vk::FormatProperties2 formatProperties = + physicalDeviceCandidate.getFormatProperties2(mainTextureFormat); + const vk::FormatFeatureFlags requiredFeatures = + vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eBlitDst | + vk::FormatFeatureFlagBits::eSampledImageFilterLinear; + if ((formatProperties.formatProperties.optimalTilingFeatures & requiredFeatures) != + requiredFeatures) { + valid = false; + } + } + // If this candidate supports everything the tutorial needs, select it and stop searching. + // This is fine for a tutorial, but a proper application might score all valid devices and choose the + // best one. + if (valid) { + selectedCandidate = vkbPhysicalDeviceCandidate; + break; + } + } + util::require(selectedCandidate.has_value(), + "No Vulkan physical device supports linear-blit mip generation for R8G8B8A8_UNORM textures"); + } + + // Store selected physical device and its properties. + const vkb::PhysicalDevice physicalDevice = *selectedCandidate; + const vk::PhysicalDevice selectedPhysicalDevice{physicalDevice.physical_device}; + + // Load physical-device properties that are needed later. + { + // After selecting the physical device we will use, + // we need to query its properties and features to cache the ones we will need later. + // We are doing this during initialization to avoid querying properties and features multiple times later. + + // Cache memory type flags so buffer and image allocation can choose compatible memory. + { + const vk::PhysicalDeviceMemoryProperties2 memoryProperties = + selectedPhysicalDevice.getMemoryProperties2(); + m_memoryTypeFlags.clear(); + + // Small optimization: reserve space in the vector to avoid multiple allocations. + m_memoryTypeFlags.reserve(memoryProperties.memoryProperties.memoryTypeCount); + + for (std::uint32_t i = 0; i < memoryProperties.memoryProperties.memoryTypeCount; ++i) { + m_memoryTypeFlags.push_back(memoryProperties.memoryProperties.memoryTypes[i].propertyFlags); + } + } + + // Cache descriptor heap properties for allocation and alignment of descriptors. + { + // Query physical device properties with a pNext chain. + vk::PhysicalDeviceDescriptorHeapPropertiesEXT heapProperties{}; + vk::PhysicalDeviceShaderObjectPropertiesEXT shaderObjectProperties{ + .pNext = &heapProperties, + }; + vk::PhysicalDeviceProperties2 properties2{ + .pNext = &shaderObjectProperties, + }; + selectedPhysicalDevice.getProperties2(&properties2); + + // Query descriptor heap descriptor sizes from physical-device properties. + { + // These values are more specific than the ones in PhysicalDeviceDescriptorHeapPropertiesEXT. + // They are smaller and more granular, allowing descriptors to be packed more tightly in the heap. + m_descriptorHeaps.m_sampledImageSize = + selectedPhysicalDevice.getDescriptorSizeEXT(vk::DescriptorType::eSampledImage); + m_descriptorHeaps.m_samplerSize = + selectedPhysicalDevice.getDescriptorSizeEXT(vk::DescriptorType::eSampler); + m_descriptorHeaps.m_uniformBufferSize = + selectedPhysicalDevice.getDescriptorSizeEXT(vk::DescriptorType::eUniformBuffer); + m_descriptorHeaps.m_storageBufferSize = + selectedPhysicalDevice.getDescriptorSizeEXT(vk::DescriptorType::eStorageBuffer); + } + + // Cache descriptor heap properties. These are mostly alignment requirements that we need to respect + // when allocating and writing to descriptor heaps. + { + // Alignment requirements for offsets of particular descriptor types. + m_descriptorHeaps.m_imageDescriptorAlignment = heapProperties.imageDescriptorAlignment; + m_descriptorHeaps.m_bufferDescriptorAlignment = heapProperties.bufferDescriptorAlignment; + m_descriptorHeaps.m_samplerDescriptorAlignment = heapProperties.samplerDescriptorAlignment; + + // Alignment requirements for the actual heap in the GPU. + m_descriptorHeaps.m_resourceHeap.m_alignment = heapProperties.resourceHeapAlignment; + m_descriptorHeaps.m_samplerHeap.m_alignment = heapProperties.samplerHeapAlignment; + + // The driver needs a minimum reserved range in the heap for internal operations. + m_descriptorHeaps.m_resourceHeap.m_minReservedRange = heapProperties.minResourceHeapReservedRange; + m_descriptorHeaps.m_samplerHeap.m_minReservedRange = heapProperties.minSamplerHeapReservedRange; + + // Push data is supposed to be small. Most devices have a limited amount of data we can push. + m_descriptorHeaps.m_maxPushDataSize = heapProperties.maxPushDataSize; + + // Camera and image descriptors are stored contiguously. + // We calculate an aligned stride for simplicity. This is the aligned size of the descriptor. + // This makes indexing in the descriptor heap easier. + { + m_descriptorHeaps.m_uniformBufferStride = util::safeCastToU32(util::alignUp( + m_descriptorHeaps.m_uniformBufferSize, m_descriptorHeaps.m_bufferDescriptorAlignment)); + + m_descriptorHeaps.m_sampledImageStride = util::safeCastToU32(util::alignUp( + m_descriptorHeaps.m_sampledImageSize, m_descriptorHeaps.m_imageDescriptorAlignment)); + } + } + + // We have a small shader cache. We need to store our current shader binary properties to know if the + // cached binary is valid or not. + { + std::copy(shaderObjectProperties.shaderBinaryUUID.begin(), + shaderObjectProperties.shaderBinaryUUID.end(), + m_shaderBinaryCacheProperties.m_shaderBinaryUUID.begin()); + m_shaderBinaryCacheProperties.m_shaderBinaryVersion = shaderObjectProperties.shaderBinaryVersion; + } + } + } + + // Create the logical device. + { + // After selecting the physical device, we need to create a logical device to interface with it. + // We need to select and enable the features and extensions we need for our application. + // We do so using vk-bootstrap, which reuses the physical device selector to create a device with the same + // criteria as the physical device selection, allowing us to skip recreating a pNext chain with the features + // we need to enable. + + m_vkbData.m_device = + vkbGetIfValid(vkb::DeviceBuilder(physicalDevice).build(), "Creating Vulkan logical device"); + + m_logicalDevice = vk::Device{m_vkbData.m_device.device}; + + // Load device-level function pointers for Vulkan-Hpp wrappers. + // Before we only loaded the function pointers for the instance-level functions. + // After creating the logical device, we can load the function pointers that depend on it to skip the + // loader. Loading the function pointers is a bit tedious, but is handled automatically by Vulkan-Hpp. + // Other libraries like Volk provide similar functionality for the C API. + VULKAN_HPP_DEFAULT_DISPATCHER.init(m_vkbData.m_instance.instance, vkGetInstanceProcAddr, + m_vkbData.m_device.device); + } + } + + // Fetch queues using vk-bootstrap. + { + + // Queues are the main way to submit work to the GPU in Vulkan. + // In Vulkan, you record a command buffer with work and then submit it to a queue to execute it. + // Queues are key for advanced GPU parallelism. + // However, it is still viable for most applications to use a universal queue for all operations. + // + // Queues are created with the logical device creation, but vk-bootstrap handles that for us. + + // The graphics queue allows us to submit and execute draw commands. + m_graphicsQueue = + vk::Queue{vkbGetIfValid(m_vkbData.m_device.get_queue(vkb::QueueType::graphics), "Getting graphics queue")}; + + // The present queue will allow us to present rendered images to the surface. + m_presentQueue = + vk::Queue{vkbGetIfValid(m_vkbData.m_device.get_queue(vkb::QueueType::present), "Getting present queue")}; + + // Multiple Vulkan objects need the queue family index to specify which queue family they depend on. + m_graphicsQueueFamily = vkbGetIfValid(m_vkbData.m_device.get_queue_index(vkb::QueueType::graphics), + "Getting graphics queue family"); + + const uint32_t presentQueueFamily = + vkbGetIfValid(m_vkbData.m_device.get_queue_index(vkb::QueueType::present), "Getting present queue family"); + + // This tutorial assumes graphics and present queues are from the same family for simplicity. + // Separate-family presentation is intentionally omitted. + // This beginner tutorial keeps image ownership simple by avoiding queue ownership transfers. + util::require(m_graphicsQueueFamily == presentQueueFamily, + "This tutorial requires graphics and present queues from the same family"); + } + + // Create the swapchain using vk-bootstrap. + { + // Vulkan uses the swapchain to manage how images are presented to the surface. + // The swapchain is a list of images that are presented to the surface. + // The swapchain is designed to synchronize presentation latency and screen refresh. + + // Selecting the present mode is important for performance and latency. + // Immediate mode presents images as soon as possible, but can cause visible tearing. + // FIFO is used by this tutorial and by most applications, as it is widely supported. + // FIFO will provide a queue of images to present, and display the next image at display refresh. + // The application will render into an image in the queue, avoiding tearing. + const VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; + + // The process of creating a swapchain is a bit tedious. + // We need to query the surface capabilities and select the best format and present mode. + // Then we need to create the swapchain, vk-bootstrap handles all of this for us. + { + vkb::SwapchainBuilder swapchainBuilder = + vkb::SwapchainBuilder(m_vkbData.m_device) + .set_desired_extent(windowWidth, windowHeight) + .set_desired_present_mode(presentMode) + // The app renders directly into swapchain images as color attachments. + .add_image_usage_flags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + m_vkbData.m_swapchain = vkbGetIfValid(swapchainBuilder.build(), "Creating Vulkan swapchain"); + } + + // Store format and extent of the swapchain images for later use. + { + m_swapchainFormat = vk::Format{m_vkbData.m_swapchain.image_format}; + m_swapchainExtent = vk::Extent2D{ + .width = m_vkbData.m_swapchain.extent.width, + .height = m_vkbData.m_swapchain.extent.height, + }; + } + + // Retrieve swapchain images and create views for them. + { + // Swapchain images are owned by the swapchain, so we only retrieve their handles. + // This implies that we do not need to create or destroy the swapchain images, + // but we do need to create image views for them. + // Vulkan implementations have a minimum number of images they require for the swapchain. + // On some devices like Android phones, the minimum number of images can be up to 10. + // Remember that the number of swapchain images is independent of the number of frames in flight, which + // is usually 2. vk-bootstrap handles the minimum number of swapchain images for us. + const std::vector images = vkbGetIfValid(m_vkbData.m_swapchain.get_images(), "Getting swapchain images"); + + // Small optimization: reserve space in the vector to avoid multiple allocations. + m_swapchainImages.reserve(images.size()); + + for (VkImage image : images) { + + // vk-bootstrap returns a Vulkan API C handle, so we wrap it in a Vulkan-Hpp wrapper. + const vk::Image swapchainImage{image}; + + // Image views are used to select the particular part of an image used by a resource. + // They allow us to render or copy to a particular mip level or layer of an image. + // Descriptor heaps remove the need to create image view objects for descriptors, + // but we still need to create image views for some use cases like attachments. + // We need to create image views for the swapchain images, so that we can render into them. + const vk::ImageViewCreateInfo imageViewInfo{ + .image = swapchainImage, + .viewType = vk::ImageViewType::e2D, + .format = m_swapchainFormat, + .components = identityComponentMapping, + .subresourceRange = CreateImageSubresourceRange(), + }; + m_swapchainImages.emplace_back(SwapchainImageResources{ + .m_image = swapchainImage, + .m_imageView = m_logicalDevice.createImageViewUnique(imageViewInfo), + .m_state = ImageState{.m_aspectMask = vk::ImageAspectFlagBits::eColor}, + }); + const std::size_t swapchainImageIndex = m_swapchainImages.size() - 1U; + + // Set a debug name for all Vulkan objects. + setDebugName(swapchainImage, vk::ObjectType::eImage, + std::format("SwapchainImage[{}]", swapchainImageIndex)); + setDebugName(*m_swapchainImages.back().m_imageView, vk::ObjectType::eImageView, + std::format("SwapchainImageView[{}]", swapchainImageIndex)); + } + } + } +} + +void Application::initCommandPool() +{ + // In Vulkan, we do not issue work to the GPU directly. + // Instead we record commands into command buffers and submit them to queues. + // Recording commands into command buffers allows Vulkan to record work in a multithreaded manner. + // + // Command buffers can be recorded once and submitted multiple times. + // However, this is rarely useful in practice because command buffers depend on frames in flight and swapchain + // images. Moreover, most games are very dynamic and need to record commands every frame. + // + // Command buffers are allocated from a command pool. + // Command pools manage the memory used to store command buffers. + // Command pools must be externally synchronized, so we need to use multiple pools for multithreaded command + // recording. + // + // Note: command buffers and command pools are key for multithreaded rendering, but this tutorial does not use + // multithreading for simplicity. + + util::log_msg("[Init] Init CommandPool"); + + const vk::CommandPoolCreateInfo commandPoolInfo{ + // eResetCommandBuffer allows each command buffer to be reset and recorded again. + // We re-use existing command buffers to avoid allocating new ones every frame. + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, + // These command buffers will be submitted to the graphics queue, so we need to specify the graphics queue + // family index. + // Note: we will also use the present queue for presentation, but we checked that graphics and present + // queues are from the same family for simplicity. + .queueFamilyIndex = m_graphicsQueueFamily, + }; + + m_commandPool = m_logicalDevice.createCommandPoolUnique(commandPoolInfo); + setDebugName(*m_commandPool, vk::ObjectType::eCommandPool, "MainCommandPool"); +} + +// Frames in flight are a common technique to improve performance. +// This creates objects like semaphores, fences, and command buffers for each frame in flight. +void Application::initFramesInFlightResources() +{ + // Frames in flight are a common technique to improve performance. + // The idea is to work on multiple frames in parallel, so the GPU stays busy rendering, + // avoiding stalls while waiting for presentation or CPU work from previous frames. + // + // In general, applications should have a fixed number of two frames in flight. + // This allows them to prepare the next frame while the GPU is rendering and presenting the previous one. + // Note: frames in flight is completely independent and orthogonal to the number of swapchain images, + // we will still use 2 frames in flight even if the swapchain has more than 2 images. + // + // See Vulkanised 2026: Frames in Flight Demystified - https://www.youtube.com/watch?v=Khxc_Qky-fM + // + // Since the GPU has multiple frames in flight, some resources need to be duplicated for each frame in flight. + // The tutorial duplicates the following resources for each frame in flight: semaphores, fences, command + // buffers, depth images, and camera uniform buffers. + + util::log_msg("[Init] Init Frames In Flight Resources"); + + // Vulkan is an explicit API, so it is the application's responsibility to synchronize resources. + // + // Synchronization primitives: + // - Fences: let the CPU wait for finished GPU work. + // - Semaphores: let the GPU wait for earlier GPU work, order GPU-side operations, and queue dependencies. + // - Barriers: control GPU execution and memory dependencies, visibility, etc. + // Barriers order resource access and manage layout/state transitions. + // - Events: allow more granular synchronization, are considered advanced, and are unused in this tutorial. + // + // Vulkan has multiple extensions to improve the developer experience when dealing with synchronization. + // Classic synchronization primitives are still needed, but new features like timeline semaphores, synchronization2 + // and unified image layouts are recommended for most use cases. + + // Binary semaphores are one of the basic Vulkan synchronization primitives. + // Semaphores are used to synchronize GPU work with other GPU work. + // They can only be signaled or waited on, and are used to wait and signal between GPU operations. + // Timeline semaphores are more advanced and solve many problems with binary semaphores. + // However, swapchain presentation still requires binary semaphores. + // + // This semaphore is signaled by acquireNextImageKHR once a swapchain image can be rendered to. + const vk::SemaphoreCreateInfo semaphoreInfo{}; + + // Fences are one of the basic Vulkan synchronization primitives. + // Fences synchronize GPU work with the CPU. + // + // This fence is used to ensure resources from the frame are not used until they are ready. + const vk::FenceCreateInfo fenceInfo{ + // Start fences as signaled so the first frame does not wait forever for previous work. + .flags = vk::FenceCreateFlagBits::eSignaled, + }; + + // Vulkan records commands into command buffers and submits them to queues for execution. + // This allows Vulkan to select the work the GPU will be doing. + // + // Allocate all command buffers. + const vk::CommandBufferAllocateInfo allocateInfo{ + .commandPool = *m_commandPool, + // Primary buffers can be submitted directly to a queue. + .level = vk::CommandBufferLevel::ePrimary, + // Create one command buffer per frame plus one helper buffer for setup-time uploads. + .commandBufferCount = util::safeCastToU32(m_framesInFlight.size() + 1), + }; + + // Obtain all command buffers. + const std::vector commandBuffers = m_logicalDevice.allocateCommandBuffers(allocateInfo); + util::require(commandBuffers.size() == (m_framesInFlight.size() + 1), + "Expected one command buffer per frame in flight plus one helper command buffer"); + + // Loop to initialize resources for each frame in flight. + for (std::size_t frameIndex = 0; frameIndex < m_framesInFlight.size(); ++frameIndex) { + FrameInFlightResources& frame = m_framesInFlight[frameIndex]; + + frame.m_commandBuffer = commandBuffers[frameIndex]; + + // Camera data is usually updated on each frame, so we duplicate the resource. + // This allows the CPU to update the camera while the previous frame is still rendering with the previous + // camera buffer. + frame.m_camera = createBuffer( + sizeof(CameraData), vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + std::format("FrameCameraBuffer[{}]", frameIndex)); + + // This semaphore is signaled by acquireNextImageKHR once a swapchain image can be rendered to. + // This allows the graphics queue to wait on this per-frame semaphore before rendering into the acquired + // swapchain image. + frame.m_imageAvailableSemaphore = m_logicalDevice.createSemaphoreUnique(semaphoreInfo); + + // This fence is used to ensure resources from the frame are not used until they are ready. + frame.m_inFlightFence = m_logicalDevice.createFenceUnique(fenceInfo); + + // Note: We are using the helper command buffer to transition the depth images after creating them. + // So we create and initialize the depth images later with the other images, see initDepthImages. + + // Set debug names for all Vulkan objects related to the frame in flight for easier debugging. + { + setDebugName(frame.m_commandBuffer, vk::ObjectType::eCommandBuffer, + std::format("FrameCommandBuffer[{}]", frameIndex)); + setDebugName(*frame.m_imageAvailableSemaphore, vk::ObjectType::eSemaphore, + std::format("ImageAvailableSemaphore[{}]", frameIndex)); + setDebugName(*frame.m_inFlightFence, vk::ObjectType::eFence, + std::format("FrameInFlightFence[{}]", frameIndex)); + } + } + + // Create the helper command buffer for immediate upload operations outside of the render loop. + // This helper command buffer is used for operations like uploading textures or generating mipmaps. + m_helperCommandBuffer.m_commandBuffer = commandBuffers.back(); + m_helperCommandBuffer.m_active = false; + setDebugName(m_helperCommandBuffer.m_commandBuffer, vk::ObjectType::eCommandBuffer, "HelperCommandBuffer"); +} + +void Application::initSwapchainImageSyncObjects() +{ + // We need to initialize some synchronization objects for each swapchain image. + util::log_msg("[Init] Init SwapchainImageSyncObjects"); + + // Binary semaphores are one of the basic Vulkan synchronization primitives. + // Semaphores are used to synchronize GPU work with other GPU work. + const vk::SemaphoreCreateInfo semaphoreInfo{}; + + for (std::uint32_t swapchainImageIndex = 0; swapchainImageIndex < m_swapchainImages.size(); ++swapchainImageIndex) { + SwapchainImageResources& swapchainImage = m_swapchainImages[swapchainImageIndex]; + + // Present waits on this semaphore before it displays the image. + swapchainImage.m_renderFinishedSemaphore = m_logicalDevice.createSemaphoreUnique(semaphoreInfo); + + setDebugName(*swapchainImage.m_renderFinishedSemaphore, vk::ObjectType::eSemaphore, + std::format("RenderFinishedSemaphore[{}]", swapchainImageIndex)); + + // This state tracks the swapchain image between frames and helps create barriers. + // We need to select the default aspect mask for the swapchain image, which is color. + // But we use the default layout of the swapchain image, which is undefined. + // The first time we acquire the swapchain image, we will transition it to the correct layout for rendering. + swapchainImage.m_state = ImageState{.m_aspectMask = vk::ImageAspectFlagBits::eColor}; + } +} + +void Application::initDepthImages() +{ + // When we are rendering multiple objects, Vulkan needs to know which objects should overwrite others. + // This is done using a depth buffer, which stores the depth of each pixel in the scene. + // + // In Vulkan, we need to create a depth image and a depth image view. During rendering, objects + // will write their depth to the depth image, and we will decide which fragments to keep based on their depth + // values. This process is called depth testing or Z testing. + // + // This tutorial uses a depth image per frame in flight. + // + // Note: If we wanted to implement resize, we would need to recreate the depth images along with the swapchain + // images, since the depth images depend on the swapchain extent. + // Note: The number of depth images depends on the number of frames in flight, not the number of swapchain + // images. This is easy to miss in traditional framebuffer-per-swapchain-image implementations. + util::log_msg("[Init] Init DepthImages"); + + // Keep one depth attachment per frame in flight so overlapping frames never render into the same depth image. + + // Obtain the helper command buffer for the depth image layout transition. + const vk::CommandBuffer commandBuffer = getHelperCommandBuffer(); + + for (std::uint32_t frameIndex = 0; frameIndex < m_framesInFlight.size(); ++frameIndex) { + GpuViewImage& depthImage = m_framesInFlight[frameIndex].m_depthImage; + + // Create the depth image and allocate memory for it. + { + // Depth images are normal device-local images that this app owns, unlike swapchain images. + // Since the app owns the depth images, we need to create them and allocate memory for them. + const vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = depthFormat, + .extent = + vk::Extent3D{.width = m_swapchainExtent.width, .height = m_swapchainExtent.height, .depth = 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eDepthStencilAttachment, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + depthImage.m_image.m_image = m_logicalDevice.createImageUnique(imageInfo); + depthImage.m_image.m_states.resize(1, ImageState{.m_aspectMask = vk::ImageAspectFlagBits::eDepth}); + setDebugName(*depthImage.m_image.m_image, vk::ObjectType::eImage, + std::format("DepthImage[{}]", frameIndex)); + } + + // Allocate memory and bind it to the image. + allocateGpuImage(depthImage.m_image); + + // Create an image view for the depth image. + { + // Image views are used to select the particular part of an image used by a resource. + // Rendering uses image views, not raw images, so create one depth view per depth image. + const vk::ImageViewCreateInfo viewInfo{ + .image = *depthImage.m_image.m_image, + .viewType = vk::ImageViewType::e2D, + .format = depthFormat, + .subresourceRange = CreateImageSubresourceRange(0, vk::ImageAspectFlagBits::eDepth), + }; + depthImage.m_imageView = m_logicalDevice.createImageViewUnique(viewInfo); + setDebugName(*depthImage.m_imageView, vk::ObjectType::eImageView, + std::format("DepthImageView[{}]", frameIndex)); + } + + // The depth attachment will be written by depth testing during rendering. + // Each depth image is paired with one frame in flight and is only used as a depth attachment. + // Because it never becomes a present image, or has any other use, this one setup-time transition is enough; + // the per-frame fence prevents reuse while older rendering work may still touch it. + depthImage.m_image.transition(commandBuffer, + ImageState{ + .m_layout = depthAttachmentLayout, + .m_stageMask = vk::PipelineStageFlagBits2::eEarlyFragmentTests, + .m_accessMask = vk::AccessFlagBits2::eDepthStencilAttachmentWrite, + .m_aspectMask = vk::ImageAspectFlagBits::eDepth, + }); + } +} + +//**************** +// Scene initialization +//**************** + +void Application::initSceneResources() +{ + util::log_msg("[Init] Init Scene Resources"); + + const std::filesystem::path dataDir = SIGGRAPH_DATA_DIR; + std::array gltfPaths{}; + for (std::size_t i = 0; i < SceneData::sceneFileNames.size(); ++i) { + gltfPaths[i] = dataDir / std::string{SceneData::sceneFileNames[i]}; + util::log_msg("[Init] glTF scene: {}", gltfPaths[i].string()); + } + + // This tutorial stores the data to draw the scene in multiple vectors. + // * m_meshes stores the GPU mesh data. The mesh data is the geometry of the scene. + // For each mesh, we store the vertex and index buffers on the GPU. + // * m_textures stores the GPU texture data. + // * m_drawData stores the draw data. Each draw data entry describes a single draw call, and contains the mesh + // id, object id, shader variant, and texture ids. When rendering, we will loop over the draw data and issue a + // draw call for each entry, binding the shaders and corresponding descriptors. + // + // glTF is a popular open format developed by the Khronos group for 3D scenes and models. + // It is designed to be efficient to transmit and load, and is widely supported by 3D content creation tools. + // + // This function will parse the glTF files and load the scene data into CPU-side structures. + // We have already implemented a glTF parser that returns an easy-to-use structure for our application, but we + // still need to upload the data to the GPU and create the draw data. + // + // Note: our application supports loading and merging multiple glTF files. + // Some assets are distributed as a series of glTF files with different layers. + util::log_msg("[Init] Reading glTF files"); + const util::gltf::ParsedData gltfData = util::gltf::parseGltfFiles(gltfPaths); + + // We store the object data in this CPU-side vector. + // The CPU copy can be deleted after uploading it to the GPU. + std::vector objects; + + util::log_msg("[Init] Uploading Scene Meshes"); + initSceneMeshesAndDrawData(gltfData, objects); + + util::log_msg("[Init] Init Scene Camera"); + initSceneCamera(gltfData); + + util::log_msg("[Init] Init Object Buffer"); + initObjectBuffer(objects); + + util::log_msg("[Init] Init Point Light"); + initPointLight(gltfData); + + util::log_msg("[Init] Init Solid Color"); + initSolidColor(); + + util::log_msg("[Init] Init Scene Textures"); + initSceneTextures(gltfData); +} + +void Application::initSceneMeshesAndDrawData(const util::gltf::ParsedData& gltfData, std::vector& objects) +{ + // glTF parsing gives us a CPU-side scene description. + // This step turns that description into data we will upload to the GPU for rendering. + // We will also keep some data on the CPU to be able to issue the draw calls and bind descriptors. + + objects.resize(gltfData.nodes.size()); + + // Small optimization: reserve space in the vectors to avoid reallocations. + // Note: we are storing indices into the vectors instead of references. + // This has slight overhead but indices will still be valid if the vector reallocates. + m_scene.m_meshes.reserve(gltfData.meshes.size()); + m_scene.m_drawData.reserve(gltfData.nodes.size()); + + // Get glTF data for meshes and upload it to the GPU. + const std::uint32_t meshCount = util::safeCastToU32(gltfData.meshes.size()); + for (std::uint32_t meshId = 0; meshId < meshCount; ++meshId) { + + // Generate the buffers with the mesh data for this mesh. + const util::MeshGeometryData geometry = util::gltf::buildMeshGeometryData(meshId, gltfData); + + // Note: we only keep the GPU copy of the mesh data, and discard the CPU copy after uploading it to the GPU. + const GpuMesh& gpuMesh = m_scene.m_meshes.emplace_back(GpuMesh{ + // For simplicity we use a single vertex buffer per mesh in the tutorial. + // Some GPU implementations would benefit if the application splits vertex data into position and + // non-positional attributes. + .m_vertices = uploadToNewGpuBuffer(std::as_bytes(std::span{geometry.vertices}), + vk::BufferUsageFlagBits::eVertexBuffer, + std::format("{} VertexBuffer", geometry.name)), + // For most meshes, vertices are reused across multiple triangles, so we use an index buffer to avoid + // duplicating vertex data. + .m_indices = + uploadToNewGpuBuffer(std::as_bytes(std::span{geometry.indices}), vk::BufferUsageFlagBits::eIndexBuffer, + std::format("{} IndexBuffer", geometry.name)), + .m_indexCount = util::safeCastToU32(geometry.indices.size()), + }); + + // Validate the mesh data. + { + util::require(gpuMesh.m_indexCount > 0, std::format("{} has a mesh without any indices", geometry.name)); + + util::require(gpuMesh.m_indices.m_size == + (static_cast(gpuMesh.m_indexCount) * sizeof(std::uint32_t)), + std::format("{} index buffer byte size does not match stored index count", geometry.name)); + + // Check that indices are inside the vertex range. + // Note: m_indexCount > 0 so geometry.indices.size() > 0. + util::require(*std::max_element(geometry.indices.begin(), geometry.indices.end()) < + geometry.vertices.size(), + std::format("{} has an index outside its vertex range.", geometry.name)); + + util::require((gpuMesh.m_vertices.m_size > 0) && + (gpuMesh.m_vertices.m_size == (sizeof(Vertex) * geometry.vertices.size())), + std::format("{} has a mesh with an invalid vertex buffer size of {} ({} * {})", geometry.name, + gpuMesh.m_vertices.m_size, sizeof(Vertex), geometry.vertices.size())); + } + } + + // Get node data for the glTF and store it for draws and descriptors. + // + // In a normal game, it is common for the same mesh to be reused multiple times in the scene, but with different + // transforms and materials. Similarly, it is also common for the same texture to be reused multiple times in + // the scene, but by different meshes and materials. The easiest way to handle this is to store the mesh and + // texture data once, and then have multiple draw calls that reference the same mesh data. + // + // In this tutorial draw data is stored in the CPU and used to issue the draw calls. + const std::uint32_t nodeCount = util::safeCastToU32(gltfData.nodes.size()); + for (std::uint32_t nodeIndex = 0; nodeIndex < nodeCount; ++nodeIndex) { + + const util::gltf::Node& node = gltfData.nodes[nodeIndex]; + + objects[nodeIndex].model = util::math::generateModel(node.pos, node.eulerAngles, node.scale); + + util::require(node.meshId < m_scene.m_meshes.size(), "glTF node references an invalid mesh id"); + + // We use a special shader for objects with an albedo texture but not a normal map. + // Depending on your architecture and GPU, using a default texture for the normal map might be better. + const ShaderVariant shaderVariant = + node.albedoTextureId == util::gltf::invalidGltfId + ? ShaderVariant::SolidColor + : (node.normalTextureId == util::gltf::invalidGltfId ? ShaderVariant::Albedo + : ShaderVariant::AlbedoAndNormal); + + m_scene.m_drawData.push_back(SceneDraw{ + .m_meshId = node.meshId, + .m_objectIndex = nodeIndex, + .m_shaderVariant = shaderVariant, + .m_albedoTextureIndex = node.albedoTextureId, + .m_normalTextureIndex = node.normalTextureId, + .m_debugName = node.name, + }); + } +} + +void Application::initSceneCamera(const util::gltf::ParsedData& gltfData) +{ + // The parsed glTF camera becomes the runtime free-camera starting point. + // Each frame has a persistently mapped camera buffer so the CPU can update one frame slot without waiting for other + // frames that may still be executing on the GPU. + m_cameraPos = gltfData.cameraPos; + const auto [cameraYaw, cameraPitch] = util::math::calculateYawPitch(gltfData.cameraPos, gltfData.cameraLookAt); + m_cameraYaw = cameraYaw; + m_cameraPitch = cameraPitch; + + for (FrameInFlightResources& frame : m_framesInFlight) { + uploadCameraData(frame); + } +} + +void Application::initObjectBuffer(std::span objects) +{ + // This buffer contains data for each node. + // In this tutorial, the only data we store is the model matrix. + // In a real application, we would also store other per-object data like animation data, etc. + // + // In our tutorial the vertex shader reads one model matrix per draw from this storage buffer. + // The draw's firstInstance value is used to index into the buffer and select the correct object data. + m_scene.m_objects = uploadToNewGpuBuffer( + std::as_bytes(objects), vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + "SceneObjectBuffer"); +} + +void Application::initPointLight(const util::gltf::ParsedData& gltfData) +{ + // In the tutorial, we hard-coded a single point light for the scene and store it as a uniform buffer on the GPU. + const glm::vec3 lightPosition = gltfData.cameraLookAt + glm::vec3{0.0F, 4.0F, 2.0F}; + const LightData light{ + .pointPosition = lightPosition, + .pointIntensity = 0.90F, + .ambientColor = glm::vec3{1.0F, 0.92F, 0.78F}, + .ambientIntensity = 0.12F, + }; + m_scene.m_pointLight = + uploadToNewGpuBuffer(std::as_bytes(std::span{&light, std::size_t{1}}), + vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + "ScenePointLightBuffer"); +} + +void Application::initSolidColor() +{ + // We have a shader using a fixed solid color. + // Currently the tutorial sets this color to a hard-coded value in a UBO. + // A real application would likely have a fixed color per object, and might send this data as push data, rather + // than a UBO. + const SolidColorData solidColor{ + // Official red tone of the Vulkan logo. + .color = glm::vec4{util::sRgbToLinear(164, 30, 34), 1.0F}, + }; + m_scene.m_solidColor = + uploadToNewGpuBuffer(std::as_bytes(std::span{&solidColor, std::size_t{1}}), + vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress, + "SceneSolidColorBuffer"); +} + +void Application::initSceneTextures(const util::gltf::ParsedData& gltfData) +{ + // From the Vulkan point of view, normal, albedo, and all other textures are the same type of object and + // resource. So they are treated the same way in this tutorial and stored together in this vector. + + // Small optimization: reserve space in the vector to avoid reallocations. + // We store indices into this vector in the draw data. + // This has slight overhead but allows us to avoid invalidating references when the vector is resized. + m_scene.m_textures.reserve(gltfData.textures.size()); + + // Read all textures in the glTF and store them as GPU images. + for (const util::gltf::GltfTexture& textureInfo : gltfData.textures) { + util::log_msg("[Init] Reading Texture: {}", textureInfo.filename); + // Read pixel data and store it in the CPU. This will be deleted after uploading it to the GPU. + const util::ImageRgba8 texture = util::readImageFileRgba8(textureInfo.filename); + + // Upload texture to the GPU and generate mips. + m_scene.m_textures.push_back(createTexture(texture, textureInfo.name)); + } + util::require(!m_scene.m_textures.empty(), "Textures expected. The glTF file appears incorrect"); +} + +//**************** +// Shader and descriptor setup +//**************** + +void Application::initDescriptorHeaps() +{ + util::log_msg("[Init] Init Descriptor Heaps"); + + // In Vulkan, descriptors describe resources such as buffers and images to a shader. + // Descriptors are opaque data blobs that describe a resource like a buffer or an image. + // They usually contain a pointer to the resource, and some metadata about how the resource is used. + // + // Classic Vulkan tutorials usually teach descriptor set layouts, descriptor pools, descriptor sets, and + // pipeline layouts. This tutorial uses EXT_descriptor_heap instead. The shader still declares normal + // set/binding resources, but shader-object creation receives a mapping table that translates those bindings + // into byte offsets in these heap buffers. + // + // Descriptor heaps are more flexible than descriptor sets. + // Descriptors are opaque blobs stored in memory. The application can change the descriptor bound to a shader by + // writing to this memory, including copying descriptors and passing them around in shaders. + // + // Descriptor heaps are also more manual. We calculate the space needed for the descriptors and allocate GPU + // buffers to hold them. The application is responsible for managing and writing heap memory. + // + // The mapping table links shader bindings to byte offsets in the heap. + // Applications can write descriptors directly into heap buffers, then select which descriptor is accessed + // from a shader by mapping bindings to offsets in this buffer. The data flow in this tutorial is: + // shader binding -> descriptor heap mapping -> heap byte offset -> read descriptor bytes from heap -> resource. + // + // It is possible to use descriptor heaps with untyped pointers, and to skip mappings and shader sets and bindings. + // This tutorial uses shader bindings and mapping tables to make the code easier to read and understand. + // We only use constant offsets and push indices for resource mapping. + // When porting existing applications, it is worth considering other mapping sources supported by descriptor heaps. + // + // It is important to highlight that descriptor heaps are a different way of working with descriptors. + // Descriptor heaps are based on manual memory management, which requires a lot more manual work. + // When selecting the physical device in initVulkanVKB, we need to cache the selected device's descriptor sizes + // and alignments alongside the heap alignments. Then we need to manage the offsets in the heap manually, to + // ensure that all descriptors are correctly aligned. Descriptor heaps allow more advanced use cases not + // explored in this tutorial. For example, they allow descriptor data to be copied and moved using GPU shaders. + + // Calculate descriptor byte offsets in the descriptor heap. + { + // With descriptor heaps, we need to manage two heaps, one for resources and one for samplers. + // We start by managing the resource heap and reserving space for each resource descriptor. + // + // We will calculate the offset of each descriptor in the resource descriptor heap. + // Our code needs to ensure that the descriptor offsets are correctly aligned. After allocating all descriptors, + // we will obtain the total size of the descriptor heap, which we can use to allocate the GPU buffer. + + // The tutorial uses a fixed resource-heap layout to keep the code simple: + // We decided to use a fixed resource heap structure with the following blocks: + // [reserved] Reserved space for internal driver usage, required by the extension. + // Can be placed anywhere in the heap, but this tutorial reserves the start of the heap. + // [camera uniform 0][camera uniform 1][ ... ] Descriptors for the buffers containing the camera data. + // We have one camera buffer per frame in flight. These descriptors are mapped using + // **push indices**, so the shader can select the correct camera buffer for the current frame. + // [object storage] This is a huge buffer containing all the object data for the scene. + // This is mapped using a **constant offset**. The shader indexes it using firstInstance. + // [point light uniform] This buffer contains the point light data for the scene. + // It is mapped using a **constant offset** and does not change in this tutorial. + // [solid color uniform] This buffer contains the solid color data for the shader variant. + // It is constant, so it is mapped using a **constant offset** and does not change in this tutorial. + // A more realistic use case might send it per draw call using push data. + // [sampled image 0][sampled image 1][ ... ] Descriptors for the sampled images. + // We have a descriptor for each texture in the scene. + // Note how combined sampled-image shader variables still read their image + // descriptor bytes from the resource heap. The sampler descriptor bytes come from the sampler heap. + // These descriptors are mapped using **push indices**; we select the correct texture for each draw + // call using the draw data. + // + // Note: The previous diagram omits padding before each block, which is required to satisfy the alignment + // requirements. + + // cursorResourceHeap is a local variable that tracks the current offset in the resource descriptor heap. + // It is used to calculate each descriptor offset and the final heap size. + // + // The first block on the heap is reserved for internal driver usage, so we reserve that space. + // Device properties report the minimum reserved range required by the implementation. + // The application must reserve this space and not access it while the heap range is bound. + // This tutorial reserves the start of the heap, but the reserved range can be placed elsewhere. + VkDeviceSize cursorResourceHeap = m_descriptorHeaps.m_resourceHeap.m_minReservedRange; + + // Advance descriptor heap offsets to allocate the resources. + { + // The basic idea is to use cursorAllocateResourceDescriptorRange to advance the cursor and reserve + // space for each descriptor in the heap. As explained above, the heap layout is fixed, so we can + // calculate the offsets for each descriptor in the heap. + + // Small helper to advance the descriptor heap offset cursor. + // It returns the aligned offset for the new descriptor, + // and reserves space in the heap to store the new descriptor. + // Note: we are not creating the descriptor bytes, just reserving space for them in the heap. + const auto cursorAllocateResourceDescriptorRange = [&cursorResourceHeap](VkDeviceSize size, + VkDeviceSize alignment) { + // Align the current position in the heap to the required descriptor alignment. + // This aligned position is returned and will be the offset of the descriptor in the heap. + cursorResourceHeap = util::alignUp(cursorResourceHeap, alignment); + + const std::uint32_t offset = util::safeCastToU32(cursorResourceHeap); + + // Reserve enough space in the heap for the new descriptor. + cursorResourceHeap += size; + + return offset; + }; + // Use the helper to advance the cursor and reserve space for each descriptor in the heap in our fixed + // layout. + // + // Note: All descriptor sizes and alignments are cached in initVulkanVKB, so we can use them here to + // calculate offsets. Descriptor alignment refers to descriptor offsets in the heap; + // resource and sampler heap alignment requirements refer to the GPU address used when binding the heap. + + // Note: we use stride (aligned-up size) to allocate all camera buffers at once. + // This makes it easier to index each buffer descriptor in the heap. + m_descriptorHeaps.m_cameraOffset = cursorAllocateResourceDescriptorRange( + static_cast(m_descriptorHeaps.m_uniformBufferStride) * m_framesInFlight.size(), + m_descriptorHeaps.m_bufferDescriptorAlignment); + + m_descriptorHeaps.m_objectsOffset = cursorAllocateResourceDescriptorRange( + m_descriptorHeaps.m_storageBufferSize, m_descriptorHeaps.m_bufferDescriptorAlignment); + m_descriptorHeaps.m_pointLightOffset = cursorAllocateResourceDescriptorRange( + m_descriptorHeaps.m_uniformBufferSize, m_descriptorHeaps.m_bufferDescriptorAlignment); + m_descriptorHeaps.m_solidColorOffset = cursorAllocateResourceDescriptorRange( + m_descriptorHeaps.m_uniformBufferSize, m_descriptorHeaps.m_bufferDescriptorAlignment); + + // Note: we use stride (aligned-up size) to allocate all images at once. + // This is similar to how we handle camera buffer descriptors. + m_descriptorHeaps.m_pushTextureOffset = cursorAllocateResourceDescriptorRange( + static_cast(m_descriptorHeaps.m_sampledImageStride) * m_scene.m_textures.size(), + m_descriptorHeaps.m_imageDescriptorAlignment); + } + + // At the end, the cursor contains the total size of the resource descriptor heap. + m_descriptorHeaps.m_resourceHeap.m_rangeSize = cursorResourceHeap; + } + // Calculate sampler heap offset. + { + // When interacting with descriptor heaps, we need to manage two heaps, one for resources and one for samplers. + // We completed our resource heap. Now we need to deal with sampler descriptors. In this tutorial, + // we only use two samplers, so the layout is even simpler than the resource heap. + // + // This tutorial only uses two samplers, so the sampler heap layout is simple: + // [reserved] Implementation-reserved range. Can be placed anywhere in the heap. + // [linear sampler] A linear sampler descriptor, + // used by all combined sampled-image mappings for albedo textures. + // [nearest sampler] A nearest sampler descriptor, + // used by all combined sampled-image mappings for normal maps. + + // Reserve implementation space at the beginning of the sampler heap, similar to the resource heap. + // Note: the reserved range can be placed anywhere in the heap. + VkDeviceAddress cursorSamplerHeap = m_descriptorHeaps.m_samplerHeap.m_minReservedRange; + + // Align the cursor to the required alignment for sampler descriptors. + cursorSamplerHeap = util::alignUp(cursorSamplerHeap, m_descriptorHeaps.m_samplerDescriptorAlignment); + + // Store the aligned offset of the first sampler descriptor in the heap. + m_descriptorHeaps.m_linearSamplerOffset = util::safeCastToU32(cursorSamplerHeap); + + // Increase and align for the next sampler descriptor. + cursorSamplerHeap = util::alignUp(cursorSamplerHeap + m_descriptorHeaps.m_samplerSize, + m_descriptorHeaps.m_samplerDescriptorAlignment); + + m_descriptorHeaps.m_nearestSamplerOffset = util::safeCastToU32(cursorSamplerHeap); + + // Add the sampler descriptor size to the cursor to calculate the total size of the sampler heap. + m_descriptorHeaps.m_samplerHeap.m_rangeSize = cursorSamplerHeap + m_descriptorHeaps.m_samplerSize; + } + + // Allocate descriptor heap buffers. + { + + // We calculated the total size of the resource and sampler descriptor heaps. + // Now we need to allocate GPU buffers to hold the descriptor bytes. + + // This function allocates a GPU buffer for a descriptor heap. + // Sets the required usage flags, adds padding for GPU address alignment, + // and calculates the bind offset for the heap range. + auto allocateAlignedBuffer = [this](DescriptorHeapResources::DescriptorHeapData& descriptorHeapData, + const std::string& debugName) { + // Allocating a GPU buffer and ensuring it is aligned to the required GPU address alignment is + // a common operation in Vulkan. + // A new buffer's device address is not guaranteed to satisfy any GPU alignment. + // The common solution is to allocate extra padding and bind an aligned subrange inside the buffer. + // + // Vulkan specifies GPU-address alignment requirements for resource and sampler heap ranges. + // initVulkanVKB caches the alignment requirements for the selected physical device. + + // Mark the buffer as descriptor-heap memory and request a shader device address for the heap range. + const vk::BufferUsageFlags heapUsage{vk::BufferUsageFlagBits::eDescriptorHeapEXT | + vk::BufferUsageFlagBits::eShaderDeviceAddress}; + + // We allocate enough extra space that we can bind an aligned subrange inside the buffer. + // alignedSize = rangeSize + ((alignment > 0) ? (alignment - 1) : 0); + const VkDeviceSize alignedSize = + util::alignedAllocationSize(descriptorHeapData.m_rangeSize, descriptorHeapData.m_alignment); + + // We are creating a host-visible buffer so we can write descriptors directly into it. + // A proper application would use a device-local buffer and a staging buffer to upload descriptors. + const vk::MemoryPropertyFlags memoryProperties = + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent; + + // Create the GPU buffer for the descriptor heap. + descriptorHeapData.m_buffer = createBuffer(alignedSize, heapUsage, memoryProperties, debugName); + + // Bound heap ranges must start at device-aligned GPU addresses. + // We cached the offset required to move from the buffer's base address to the aligned heap base used by + // Vulkan. We use this bindOffset to calculate the vk::DeviceAddressRangeEXT used when binding the heap. + descriptorHeapData.m_bindOffset = + util::alignedOffset(descriptorHeapData.m_buffer.m_addressGPU, descriptorHeapData.m_alignment); + + // Verify the alignment math. + { + // Check the main conditions needed for an aligned heap range. + + // Check that the bound heap base is correctly aligned. + util::require(util::alignUp(descriptorHeapData.m_buffer.m_addressGPU + descriptorHeapData.m_bindOffset, + descriptorHeapData.m_alignment) == + descriptorHeapData.m_buffer.m_addressGPU + descriptorHeapData.m_bindOffset, + "Descriptor heap bind address is not aligned"); + + // Check that the buffer size is large enough. + util::require(descriptorHeapData.m_bindOffset + descriptorHeapData.m_rangeSize <= + descriptorHeapData.m_buffer.m_size, + "Aligned resource heap range does not fit in the allocated buffer"); + } + }; + + // Allocate aligned buffers for the resource and sampler descriptor heaps. + { + // We are allocating two GPU buffers: one for the resource heap and one for the sampler heap. + // We can allocate a single GPU buffer for both heaps, but this tutorial keeps them separate for simplicity. + allocateAlignedBuffer(m_descriptorHeaps.m_resourceHeap, "ResourceDescriptorHeap"); + allocateAlignedBuffer(m_descriptorHeaps.m_samplerHeap, "SamplerDescriptorHeap"); + } + } + + // Write resource heap descriptors. + { + + // Currently, the heap buffers are empty. We need to write the descriptors into the heap buffers. + // + // In previous steps we calculated the offsets and ranges for each descriptor in the heap. + // Now we need to write the descriptors to those offsets in the heap buffers. + // The descriptors are opaque bytes that describe a resource like a buffer or an image. + // They are opaque but usually contain a GPU address and some metadata about the resource. + // Before creating the descriptors, we need to create the actual resources they reference. + // In previous functions we created the GPU buffers and images for the scene, + // now we need to create the descriptors that reference those resources. + // When creating descriptors we need to add some metadata about the resource, + // like size, format, etc. In Vulkan 1.0 we needed to create ImageViews and descriptor set layouts. + // Descriptor heaps greatly simplify the process, avoiding intermediate views and creating + // descriptors directly. + // + // In summary, to write a descriptor we need to: + // - Specify the host address where writeResourceDescriptorsEXT writes the descriptor bytes. + // This tutorial uses the descriptor's final offset in the heap, instead of a temporary staging buffer. + // - Create the resource that the descriptor will reference (buffer or image). + // - Provide the metadata needed to describe the resource, such as size, format, and layout. + // + // Note: it is also possible to write descriptors to temporary host-visible memory and copy them to the final + // heap location. That can allow more performant solutions like a device-local descriptor heap, + // using a staging buffer to upload descriptors. This tutorial uses a host-visible heap for simplicity. + + // We use vectors to store descriptor create data and keep pointer targets alive until + // writeResourceDescriptorsEXT writes the descriptors. + std::vector resources; + std::vector descriptorRanges; + std::vector bufferRanges; + std::vector imageViewInfos; + std::vector imageInfos; + + // Fixed scene buffer descriptors: objects, point light, and solid color. + constexpr std::size_t fixedSceneBufferDescriptorCount = 3; + + const std::size_t cameraDescriptorCount = m_framesInFlight.size(); + const std::size_t totalBufferDescriptorCount = cameraDescriptorCount + fixedSceneBufferDescriptorCount; + const std::size_t totalImageDescriptorCount = m_scene.m_textures.size(); + const std::size_t totalDescriptorCount = totalBufferDescriptorCount + totalImageDescriptorCount; + + // Reserve all entries up front so adding elements does not invalidate those pointers. + { + // The descriptor create structs store pointers to other vector elements. + // If the vectors grow, those pointers would be invalidated before writeResourceDescriptorsEXT is called. + // Here we reserve enough space in the vectors to avoid reallocations and keep those pointers valid. + // emplaceNoRealloc enforces the exact reserve sizes below, ensuring the pointers are not invalidated. + + resources.reserve(totalDescriptorCount); + descriptorRanges.reserve(totalDescriptorCount); + + bufferRanges.reserve(totalBufferDescriptorCount); + + imageViewInfos.reserve(totalImageDescriptorCount); + imageInfos.reserve(totalImageDescriptorCount); + } + + // Helper function to emplace an element in a vector and ensure it does not reallocate. + // This keeps pointers to vector elements valid. + // We could also write each descriptor in a separate call to writeResourceDescriptorsEXT, + // or have a vector of unique_ptrs, but this is simpler and more efficient. + auto emplaceNoRealloc = [](auto& vec, auto&&... args) -> auto& { + util::require(vec.size() < vec.capacity(), "Vector exceeded reserved capacity"); + return vec.emplace_back(std::forward(args)...); + }; + + // Write resource heap descriptors. + { + util::require(m_descriptorHeaps.m_resourceHeap.m_buffer.m_addressCPU != nullptr, + "Resource descriptor heap must be CPU-mapped before writing descriptors"); + + // The heap buffer is host-visible and coherent, so writeResourceDescriptorsEXT can write descriptor + // bytes directly into mapped memory. A proper application would use a device-local buffer and a staging + // buffer to upload descriptors. + std::byte* const resourceHeapBase = + static_cast(m_descriptorHeaps.m_resourceHeap.m_buffer.m_addressCPU) + + m_descriptorHeaps.m_resourceHeap.m_bindOffset; + + // Add the create data and destination range for one buffer descriptor. + auto addBufferDescriptor = [&bufferRanges, &descriptorRanges, &resources, emplaceNoRealloc, + resourceHeapBase](vk::DescriptorType type, const GpuBuffer& buffer, + vk::DeviceSize bufferOffset, vk::DeviceSize size, + std::uint32_t heapOffset, vk::DeviceSize descriptorSize) { + // Validate that the descriptor points at a valid GPU-addressable buffer range. + { + // Ensure the buffer has a GPU address. Buffer descriptors reference device-address ranges. + util::require(buffer.m_addressGPU != 0, "Buffer descriptor requires a GPU address"); + // Ensure the offset and size are within the buffer's range. + util::require(bufferOffset <= buffer.m_size && size <= buffer.m_size - bufferOffset, + "Buffer descriptor range is outside the buffer"); + } + + // Store the buffer range, descriptor info, and destination range in vectors so all pointer targets + // stay alive until writeResourceDescriptorsEXT writes the descriptor bytes. + { + // To create a buffer descriptor we just use a Device Address Range. + // This allows us to specify the size and offset, but skips having to create a BufferView. + vk::DeviceAddressRangeEXT& bufferRange = emplaceNoRealloc( + bufferRanges, + vk::DeviceAddressRangeEXT{.address = buffer.m_addressGPU + bufferOffset, .size = size}); + + // To create a descriptor we just need to set the type and the buffer range. + // Notice that we are storing a reference to the buffer range in the descriptor, + // so we need to ensure the buffer range lives until we call writeResourceDescriptorsEXT. + vk::ResourceDescriptorInfoEXT& descriptor = + emplaceNoRealloc(resources, vk::ResourceDescriptorInfoEXT{ + .type = type, + }); + // Small workaround to initialize the union. + descriptor.data.pAddressRange = &bufferRange; + + // Set the host address range where writeResourceDescriptorsEXT will write this descriptor. + // For simplicity, the tutorial writes to the final position using the offset in the heap, + // but it is possible to write the descriptor to a temporary location and then copy it to the + // final offset in the heap. This should be preferable for performance, since the heap buffer + // can be made device-local and not host-visible. + emplaceNoRealloc(descriptorRanges, vk::HostAddressRangeEXT{ + .address = resourceHeapBase + heapOffset, + .size = static_cast(descriptorSize), + }); + } + }; + + // Add all resource-heap descriptors using the helper above. + // Note: We have a fixed layout for the resource heap, which makes this easy. + // Previously we calculated the offsets for each descriptor in the heap, + // and now we just need to write descriptors to those offsets. + + const std::uint32_t frameCount = util::safeCastToU32(m_framesInFlight.size()); + for (std::uint32_t frameIndex = 0; frameIndex < frameCount; ++frameIndex) { + // The tutorial has one camera buffer descriptor per frame in flight. + // We add one descriptor for each camera buffer. + + // Use the aligned stride calculated earlier to allocate the descriptors. + // This makes it easier to index each camera descriptor in the heap. + std::uint32_t uboWriteHeapOffset = + m_descriptorHeaps.m_cameraOffset + frameIndex * m_descriptorHeaps.m_uniformBufferStride; + + addBufferDescriptor(vk::DescriptorType::eUniformBuffer, m_framesInFlight[frameIndex].m_camera, 0, + sizeof(CameraData), uboWriteHeapOffset, m_descriptorHeaps.m_uniformBufferSize); + } + + // Storage buffer descriptor for the object data. + addBufferDescriptor(vk::DescriptorType::eStorageBuffer, m_scene.m_objects, 0, m_scene.m_objects.m_size, + m_descriptorHeaps.m_objectsOffset, m_descriptorHeaps.m_storageBufferSize); + + // Uniform buffer descriptor for the point light data. + addBufferDescriptor(vk::DescriptorType::eUniformBuffer, m_scene.m_pointLight, 0, sizeof(LightData), + m_descriptorHeaps.m_pointLightOffset, m_descriptorHeaps.m_uniformBufferSize); + + // Uniform buffer descriptor for the solid color data. + // This might be better as push data, but for simplicity we use a UBO in this tutorial. + addBufferDescriptor(vk::DescriptorType::eUniformBuffer, m_scene.m_solidColor, 0, sizeof(SolidColorData), + m_descriptorHeaps.m_solidColorOffset, m_descriptorHeaps.m_uniformBufferSize); + + const std::uint32_t textureCount = util::safeCastToU32(m_scene.m_textures.size()); + for (std::uint32_t i = 0; i < textureCount; ++i) { + // In our tutorial, we are placing all image descriptors at the end of the heap. + // This is not a requirement, but it makes it easier to index each texture descriptor in the heap + // using the draw data. + // + // Image descriptors are a bit more complicated than buffer descriptors. + // The descriptor still needs image-view metadata such as format, swizzles, and subresource range. + // writeResourceDescriptorsEXT takes VkImageViewCreateInfo directly, + // so descriptors can be created without creating an ImageView object. + + // Descriptor heaps require an ImageViewCreateInfo to create the image descriptor. + // We reuse the same struct as in Vulkan 1.0 to set the format, mip levels, layers, etc. + vk::ImageViewCreateInfo& imageViewInfo = emplaceNoRealloc( + imageViewInfos, vk::ImageViewCreateInfo{ + .image = *m_scene.m_textures[i].m_image, + .viewType = vk::ImageViewType::e2D, + .format = mainTextureFormat, + .components = identityComponentMapping, + .subresourceRange = CreateImageSubresourceRange( + 0, vk::ImageAspectFlagBits::eColor, m_scene.m_textures[i].m_mipLevels), + }); + + // Image descriptors store the image-view create info and the layout that shaders will sample from. + // The create infos only need to live until writeResourceDescriptorsEXT copies descriptor bytes below. + // Notice that with descriptor heaps, we do not need to create a separate ImageView. + vk::ImageDescriptorInfoEXT& imageInfo = + emplaceNoRealloc(imageInfos, vk::ImageDescriptorInfoEXT{ + .pView = &imageViewInfo, + .layout = vk::ImageLayout::eShaderReadOnlyOptimal, + }); + + // Initialize the descriptor with the type and image info. The descriptor stores a pointer to imageInfo, + // and imageInfo stores a pointer to imageViewInfo, so both vector elements must remain stable. + vk::ResourceDescriptorInfoEXT& descriptor = + emplaceNoRealloc(resources, vk::ResourceDescriptorInfoEXT{ + .type = vk::DescriptorType::eSampledImage, + }); + // Workaround to initialize the union. + descriptor.data.pImage = &imageInfo; + + // Set the host address range where this image descriptor will be written. + // + // Similar to camera descriptors, we use the aligned stride during allocation to make it easier to index + // each texture descriptor. Similar to buffers, we could write the descriptor to a temporary location + // and then copy it to the final offset in the heap. For performance we might employ a staging buffer to + // keep the heap device-local. + const std::uint32_t imageWriteHeapOffset = + m_descriptorHeaps.m_pushTextureOffset + i * m_descriptorHeaps.m_sampledImageStride; + emplaceNoRealloc(descriptorRanges, + vk::HostAddressRangeEXT{ + .address = resourceHeapBase + imageWriteHeapOffset, + .size = static_cast(m_descriptorHeaps.m_sampledImageSize), + }); + } + util::require(resources.size() == descriptorRanges.size(), "Resource info and range size must match"); + + util::checkVk(static_cast(m_logicalDevice.writeResourceDescriptorsEXT( + util::safeCastToU32(resources.size()), resources.data(), descriptorRanges.data())), + "Writing resource heap descriptors"); + } + } + + // Write sampler heap descriptors. + { + util::require(m_descriptorHeaps.m_samplerHeap.m_buffer.m_addressCPU != nullptr, + "Sampler descriptor heap must be CPU-mapped before writing descriptors"); + + // Samplers live in the sampler heap. + // Similar to resources, we need to write sampler descriptor bytes into the sampler heap buffer. + // + // We have a fixed layout for the sampler heap, which makes this easy. + // Currently the tutorial only uses two fixed samplers, so we need to write both descriptors into the heap. + // The combined sampled-image mappings will pair these descriptors to create a combined image sampler. + // vkWriteSamplerDescriptorsEXT takes VkSamplerCreateInfo directly, so we do not need to create a VkSampler + // object for this descriptor. + + // Use VkSamplerCreateInfo to describe the sampler descriptor. + // For teaching purposes the tutorial only uses two samplers. + // A proper application would create multiple sampler descriptors for different filtering and address modes it + // needs. + std::array samplerInfo = { + // Sampler with linear filtering and repeat address mode, which is what the Sponza albedo textures expect. + // Linear filtering allows interpolating between mip levels and texels, improving visual quality. + vk::SamplerCreateInfo{ + .magFilter = vk::Filter::eLinear, + .minFilter = vk::Filter::eLinear, + .mipmapMode = vk::SamplerMipmapMode::eLinear, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .maxLod = VK_LOD_CLAMP_NONE, + }, + // Sampler with nearest filtering and repeat address mode. + // We use nearest filtering for normal map textures to avoid interpolation. + vk::SamplerCreateInfo{ + .magFilter = vk::Filter::eNearest, + .minFilter = vk::Filter::eNearest, + .mipmapMode = vk::SamplerMipmapMode::eNearest, + .addressModeU = vk::SamplerAddressMode::eRepeat, + .addressModeV = vk::SamplerAddressMode::eRepeat, + .addressModeW = vk::SamplerAddressMode::eRepeat, + .maxLod = VK_LOD_CLAMP_NONE, + }}; + + // We use a host address range to specify where writeSamplerDescriptorsEXT will write sampler descriptors. + // Similar to resources, we are directly writing the descriptor to its final offset in the heap. + // It might be better to write it to a temporary address and then copy it to the correct offset in the heap. + // This might allow us to use a staging buffer and make the heap buffer device-local for better performance. + std::array samplerRange = { + vk::HostAddressRangeEXT{ + .address = static_cast(m_descriptorHeaps.m_samplerHeap.m_buffer.m_addressCPU) + + m_descriptorHeaps.m_samplerHeap.m_bindOffset + m_descriptorHeaps.m_linearSamplerOffset, + .size = static_cast(m_descriptorHeaps.m_samplerSize), + }, + vk::HostAddressRangeEXT{ + .address = static_cast(m_descriptorHeaps.m_samplerHeap.m_buffer.m_addressCPU) + + m_descriptorHeaps.m_samplerHeap.m_bindOffset + m_descriptorHeaps.m_nearestSamplerOffset, + .size = static_cast(m_descriptorHeaps.m_samplerSize), + }}; + + util::require(samplerInfo.size() == samplerRange.size(), "Sampler info and range size must match"); + util::checkVk(static_cast(m_logicalDevice.writeSamplerDescriptorsEXT( + util::safeCastToU32(samplerInfo.size()), samplerInfo.data(), samplerRange.data())), + "Writing sampler heap descriptor"); + } +} + +std::vector Application::buildShaderDescriptorMappings( + const std::unordered_map& shaderResourceBindings) const +{ + // In Vulkan, descriptors describe resources such as buffers and images so that shaders can access them. + // Shaders usually decorate descriptor variables with set and binding numbers, IE [[vk::binding(bindingId, setId)]]. + // In Vulkan, descriptors are grouped into descriptor sets, and each slot in a descriptor set is called a binding. + // + // Descriptor sets are mostly a legacy concept from Vulkan 1.0, where descriptors were grouped into sets. + // The idea was to group descriptors that are used or updated together into the same set, allowing applications to + // minimize descriptor writes and updates. + // + // Descriptor sets require developers to think about how descriptors should be grouped, and they also introduce a + // lot of boilerplate code for creating descriptor set layouts, allocating descriptor sets, and updating them. + // + // This tutorial uses descriptor heaps and shader objects. Descriptor heaps allow shaders to access descriptors + // directly from a GPU buffer. This avoids the need to decide how to group descriptors into descriptor sets. + // + // With descriptor heaps, we have a GPU buffer that contains descriptors, but we still need a way to map shader + // variables to the correct offsets in the descriptor heap buffer. A common way to do this is by using descriptor + // mappings. + // + // When using mappings with descriptor heaps, shaders still use the old syntax to declare set and binding numbers + // for their descriptors: [[vk::binding(bindingId, setId)]]. This allows existing shaders to work with descriptor + // heaps without changes to the shader code, making it easy to migrate existing shaders to descriptor heaps. + // + // There are multiple ways to create descriptor mappings. This tutorial uses constant offsets and push indices. + // We recommend that developers also read: + // https://docs.vulkan.org/guide/latest/descriptor_heap.html#_mapping_the_heap_to_existing_shaders + // + // Descriptor heaps offer multiple ways to map shader variables, for example: + // Descriptor heaps also offer push data as a replacement for push constants, and provide a simple way to specify + // per-draw data for the shader. Similar inline and GPU-address-based data is also supported by the extension. + // + // Note: With descriptor heaps, mappings are completely optional. Shaders do not need to specify set and binding + // numbers for their descriptors, because they can access the heap directly. This is commonly referred to as using + // untyped pointers. It can be very flexible, but it can also be error-prone and complex. + // + // Note: Slang allows developers to avoid manually specifying set and binding numbers for descriptors, and can + // automatically deduce valid bindings. Manually setting set and binding numbers is error-prone, so it is + // recommended to deduce them automatically from Slang reflection. + + // This code will create a vector of vk::DescriptorSetAndBindingMappingEXT, which will be used to map shader + // variables to the corresponding descriptor by selecting the correct offsets in the descriptor heap buffer. + // This vector will be passed when creating the shader objects. + // GPU compilers need to access the offset of each descriptor in the heap, so the offset needs to be given during + // shader compilation. + // + // Most applications create shader bindings using reflection data from the shader compiler. + // This tutorial uses Slang reflection to get the set and binding. The main idea is: + // - Iterate over the reflection data and collect all descriptors in the shaders. + // - Collect the set and binding for each descriptor using descriptor reflection data. + // - Create a new binding mapping for each descriptor. + // + // Our tutorial uses the shader variable name to select hardcoded offsets and types to identify the descriptor. + // In this tutorial, we are using a fixed layout for the descriptor heap, so we can use the offsets calculated + // previously. This is not very flexible, but it is simple and works for this tutorial. + // Most applications will have a more flexible layout for the descriptor heap, and will need to calculate and + // store the offsets for each descriptor. + // + // Our application has two shader groups, and each shader group has its own set of descriptors. + // Therefore, we create a separate mapping table for each shader group. + + // Store mappings for this group of shaders. + // Each shader group will have its own mapping table, which will be passed when creating the shader object. + std::vector mappings; + // Small optimization to reserve enough space in the vector to avoid reallocations. + mappings.reserve(shaderResourceBindings.size()); + + // Used for validation to ensure that shaders do not have duplicated shader variable names or set/binding numbers. + std::vector validateBindingNames; + std::vector> validateBindingId; + + // Helper function to create a descriptor mapping. + const auto makeDescriptorHeapMapping = [](const util::slang::ShaderResourceBinding& binding, + vk::DescriptorMappingSourceEXT source, + const vk::DescriptorMappingSourceDataEXT& sourceData) { + // This helper function uses Slang reflection data to create a descriptor mapping. + // The main idea is to select a descriptor for a shader variable by linking it to a specific offset in the + // descriptor heap that we set in sourceData. + + return vk::DescriptorSetAndBindingMappingEXT{ + // Descriptor set and binding obtained from Slang [[vk::binding(bindingId, setId)]]. + // We use Slang reflection to reduce boilerplate and hardcoded values for the binding and set numbers. + .descriptorSet = binding.set, + .firstBinding = binding.binding, + .bindingCount = 1, + .resourceMask = vk::SpirvResourceTypeFlagsEXT{binding.resourceMask}, + .source = source, + .sourceData = sourceData, + }; + }; + + // This helper calculates descriptor offsets for images using push indices with descriptor heaps. + // It creates the source data for a combined sampled image. + const auto makeCombinedImageSamplerPushIndexSourceData = [pushTextureOffset = m_descriptorHeaps.m_pushTextureOffset, + sampledImageStride = + m_descriptorHeaps.m_sampledImageStride, + samplerOffset = m_descriptorHeaps.m_linearSamplerOffset, + samplerStride = m_descriptorHeaps.m_nearestSamplerOffset - + m_descriptorHeaps.m_linearSamplerOffset]( + std::uint32_t pushOffset) { + // For combined sampled images, we use push indices. + // When using push-index mappings, the descriptor for each image is selected using data pushed to the GPU. + // This makes it easy to select the correct descriptor in the heap per-draw call, allowing each draw call to + // select a different texture per object. The final descriptor offset is calculated using: + // GPU descriptor address = resource heap start + mapping heap offset + pushed index * mapping heap stride. + // Note: we omit shader index since the tutorial only uses single-binding scalar mappings. + // + // Our application shaders declare a combined sampled image descriptor. + // This binds both the image (from the resource heap) and the sampler (from the sampler heap) into a + // single binding slot since they are a single shader variable. + // We can edit the shader to have separate image and sampler descriptors. + + util::require((pushOffset % 4) == 0, std::format("Push offsets {} must be aligned to 4 bytes", pushOffset)); + + return vk::DescriptorMappingSourceDataEXT{vk::DescriptorMappingSourcePushIndexEXT{ + // heapOffset is the constant offset added for byte calculations. + // Usually, it points to the first descriptor we can bind. + // In our case, we have a fixed layout for the heap, and all image descriptors are placed at the end of the + // heap, so it is the offset of the first image descriptor. + .heapOffset = pushTextureOffset, + // pushOffset is the offset of the pushed index in the push data. + // In our tutorial, we are using push-data texture indices to select the correct image descriptor in the + // heap, so that is our push index, which pushOffset points to. + // pushIndex = *(&PushData + pushOffset). + .pushOffset = pushOffset, + // heap index stride is the stride between each image descriptor in the heap. + // This is the number of bytes added per push index increment. + // Our stride is the aligned size of an image descriptor. This is common for use cases. + // Our application is placing images using a fixed stride to make them easier to index. + .heapIndexStride = sampledImageStride, + // Heap array stride is the stride between each array of image descriptors in the heap. + // Our mapped descriptors are single binding scalars, so our array index is always 0 and this is not + // relevant for our tutorial. This is usually set to the aligned size of an image descriptor. + // We can use a separate stride depending on the use case and layout of the heap. + .heapArrayStride = 0, + // We could specify a sampler create info in the mapping to embed a sampler in the mapping. + // Our tutorial uses a sampler from the descriptor heap. + .pEmbeddedSampler = nullptr, + // Combined image sampler mappings can be used to pack the sampler and resource index in a single uint32_t. + // If true, the combined index is set using pushOffset and ignores samplerPushOffset. + // combinedPushIndex = *(&PushData + pushOffset) = ((samplerIndex&0xFFF) << 20) | (resourceIndex&0xFFFFF) + .useCombinedImageSamplerIndex = vk::True, + // Sampler descriptor offset is calculated the same way as the image descriptor offset, + // but it is used to select the sampler descriptor in the sampler heap. + // Note: Our application is ignoring samplerPushOffset, + // because we are combining the sampler and image indices. + // This reduces the data we send to the GPU. + // Note: We could use samplerPushOffset to use different indices for the sampler and the image. + // Note: These values refer to the sampler heap offset, not the resource heap offsets. + .samplerHeapOffset = samplerOffset, + .samplerPushOffset = 0, // Unused since we are using useCombinedImageSamplerIndex. + .samplerHeapIndexStride = samplerStride, + .samplerHeapArrayStride = 0, // Unused since we only have single-binding scalar descriptors. + }}; + }; + + // We use Slang reflection to iterate through all bindings in the shader and create a mapping for each one. + for (const auto& bindingIt : shaderResourceBindings) { + + // Validation code. + { + // Check to ensure that shaders do not have duplicate set/binding names or set/binding numbers. + // This is not required, but it helps catch mistakes in shader code. + + auto nameIt = std::find(validateBindingNames.begin(), validateBindingNames.end(), bindingIt.first); + util::require(nameIt == validateBindingNames.end(), + std::format("duplicate shader resource binding name: {}", bindingIt.first)); + validateBindingNames.push_back(bindingIt.first); + + auto idPair = std::make_pair(bindingIt.second.set, bindingIt.second.binding); + auto idIt = std::find(validateBindingId.begin(), validateBindingId.end(), idPair); + util::require(idIt == validateBindingId.end(), + std::format("duplicate shader resource binding set: {} binding {}", bindingIt.second.set, + bindingIt.second.binding)); + validateBindingId.push_back(idPair); + } + + // The tutorial uses a fixed layout with few descriptors. + // We have hardcoded mappings for each descriptor in the shader, + // so we use the variable name to identify the descriptor and select the correct offset in the heap. + + // Create mapping for camera using push indices. + if (bindingIt.first == "g_camera") { + // We are using push indices so the logic is similar to our texture image mapping. + const vk::DescriptorMappingSourceDataEXT cameraSourceData{vk::DescriptorMappingSourcePushIndexEXT{ + // This points to the first camera descriptor in the heap. + .heapOffset = m_descriptorHeaps.m_cameraOffset, + // We will select the camera buffer using the current frame in flight in the push data. + .pushOffset = util::safeCastToU32(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, cameraIndex)), + .heapIndexStride = m_descriptorHeaps.m_uniformBufferStride, + }}; + + static_assert( + offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, cameraIndex) == + offsetof(DescriptorHeapDrawPushIndicesAlbedo, cameraIndex) && + offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, cameraIndex) == + offsetof(DescriptorHeapDrawPushIndicesSolidColor, cameraIndex) && + "Camera index must be at the same offset in all push data structs. This makes detecting offsets in " + "the tutorial simpler"); + + mappings.push_back(makeDescriptorHeapMapping( + bindingIt.second, vk::DescriptorMappingSourceEXT::eHeapWithPushIndex, cameraSourceData)); + } + // Create mapping for object buffer. + else if (bindingIt.first == "g_objects") { + // We have a single object buffer descriptor in the heap. + // We can use constant offset mappings to select the correct descriptor in the heap. + const vk::DescriptorMappingSourceDataEXT objectsSourceData{vk::DescriptorMappingSourceConstantOffsetEXT{ + .heapOffset = m_descriptorHeaps.m_objectsOffset, + }}; + mappings.push_back(makeDescriptorHeapMapping( + bindingIt.second, vk::DescriptorMappingSourceEXT::eHeapWithConstantOffset, objectsSourceData)); + } + // Create mapping for point light buffer. + else if (bindingIt.first == "g_pointLight") { + // We also have a single point light buffer descriptor, so we can use constant offset mappings. + const vk::DescriptorMappingSourceDataEXT pointLightSourceData{vk::DescriptorMappingSourceConstantOffsetEXT{ + .heapOffset = m_descriptorHeaps.m_pointLightOffset, + }}; + mappings.push_back(makeDescriptorHeapMapping( + bindingIt.second, vk::DescriptorMappingSourceEXT::eHeapWithConstantOffset, pointLightSourceData)); + } + // Create mapping for solid color buffer if needed. + else if (bindingIt.first == "g_solidColor") { + // We also have a single solid color buffer descriptor, so we can use constant offset mappings. + // Solid color might be better as push data, but for simplicity we use a UBO in this tutorial. + const vk::DescriptorMappingSourceDataEXT solidColorSourceData{vk::DescriptorMappingSourceConstantOffsetEXT{ + .heapOffset = m_descriptorHeaps.m_solidColorOffset, + }}; + mappings.push_back(makeDescriptorHeapMapping( + bindingIt.second, vk::DescriptorMappingSourceEXT::eHeapWithConstantOffset, solidColorSourceData)); + } + // Create mapping for albedo texture if needed. + else if (bindingIt.first == "g_albedoTexture") { + + // We select the offset of the albedo texture index. + // Notice that we are using different indices for each texture. + static_assert(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, albedoTextureIndex) == + offsetof(DescriptorHeapDrawPushIndicesAlbedo, albedoTextureIndex), + "Albedo texture index must be at the same offset in all albedo push data structs"); + const vk::DescriptorMappingSourceDataEXT albedoTextureSourceData = + makeCombinedImageSamplerPushIndexSourceData( + util::safeCastToU32(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, albedoTextureIndex))); + + // We use our helper function to create the mapping for textures. + mappings.push_back(makeDescriptorHeapMapping( + bindingIt.second, vk::DescriptorMappingSourceEXT::eHeapWithPushIndex, albedoTextureSourceData)); + } + + // Create mapping for normal texture if needed. + else { + // We select the offset of the normal texture index. + // Notice that we are using different indices for each texture. + util::require(bindingIt.first == "g_normalTexture", + std::format("Unexpected shader resource binding name: {}", bindingIt.first)); + const vk::DescriptorMappingSourceDataEXT normalTextureSourceData = + makeCombinedImageSamplerPushIndexSourceData( + util::safeCastToU32(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, normalTextureIndex))); + + // We use our helper function to create the mapping for textures. + mappings.push_back(makeDescriptorHeapMapping( + bindingIt.second, vk::DescriptorMappingSourceEXT::eHeapWithPushIndex, normalTextureSourceData)); + } + } + + return mappings; +} + +void Application::calculateVertexInputs() +{ + + // In Vulkan, geometry data is passed to the vertex shader using vertex buffers. + // Vertex buffers are GPU buffers that contain packed vertex data. + // They store vertex attributes, such as position, normal, and texture coordinates. + // + // Vulkan allows control over how to specify the layout of vertex data in the buffer, + // and how to map this vertex input data to the vertex shader input variables. + // This allows applications to optimize how shaders receive vertex data. + // + // For example, this application uses a single vertex buffer, so all vertex data is packed into one + // single struct stored in the same vertex buffer. This layout is usually called an interleaved vertex buffer. + // Alternatively, an application can use multiple vertex buffers, with each attribute or group of attributes + // coming from a separate buffer / binding. This is commonly called separate or multi-stream vertex input. + // + // The best layout for your vertex data depends on the GPU and the use case. + // Some GPUs benefit from using separate buffers for positional and non-positional data; + // check the recommendations from your GPU vendor about vertex buffers. + // + // It is important to note that most meshes reference the same vertices for multiple triangles, + // so for good performance you should also use an index buffer to avoid duplicating vertex data. + + util::log_msg("[Init] Calculate Vertex Inputs"); + + const std::filesystem::path reflectionPath{ + std::filesystem::path{SIGGRAPH_SHADER_DIR} / + std::format("{}.json", SceneData::albedoAndNormalShaderFilePair.vertexName)}; + + // Our tutorial uses Slang reflection data, which tells us which locations/formats the shader expects for the + // vertex attributes. + // We have a very simple vertex data layout. We bind a single buffer for all vertex attributes, and we pack all + // vertex data in a single struct, so calculating the vertex input data is trivial. This function just: + // - Gets the location of the shader input variables from Slang + // - Uses hardcoded names to link them to offsetof(PackedVertex, attribute) to get the offset in the buffer. + // - We always set binding = 0 since we are using a single buffer for all vertex data. + // - Sets the format of the attribute using Slang reflection data. + const util::slang::PackedVertexInputLayout layout = util::slang::calculatePackedVertexInputLayout(reflectionPath); + + // Vertex input binding descriptions reference the buffers we bound with the vertex data they contain. + // .binding = Binding slot calculated using BindVertexBuffers2 first binding + buffer index + // This will allow us to use multiple vertex buffers. + // For example some GPUs benefit from using separate buffers for positional and non-positional data. + // This is a simple tutorial. We use a single buffer for all vertex data, and we always use binding = 0. + // .stride = Distance between the start of two consecutive vertices in the buffer. + // We have all vertex data packed in a single struct, so the stride is sizeof(PackedVertex). + // When using dynamic input stride this can be modified by BindVertexBuffers2 at runtime. + // .inputRate = We are using vertex rate rather than instance rate. + m_vertexInput.m_vertexBindings = layout.bindings; + + // Vertex input attribute descriptions contain the mapping from vertex buffer data to shader input locations. + // .location = The location set by [[vk::location(n)]] in the shader. + // This identifies the shader variable that will receive the vertex data. + // .binding = binding slot set by BindVertexBuffers2. + // If we had multiple vertex buffers, this would allow us to select which buffer to read the data from. + // .offset = This identifies which data in the GPU buffer will be read for this attribute. + // It is the offset in the vertex buffer to read the data. + // In our tutorial, the data is packed in a struct so it is a simple offsetof(PackedVertex, attribute). + // Shader variable at ([[vk::location(locationId)]], for vertexIndex=vertexIndex) = + // (Bound buffer at binding=bindingId) + (bound offset) + attribute offset + bufferBinding.stride * vertexIndex + // Note: we are using vertex rate rather than instance rate. + // Note: We assume BindVertexBuffers2 binds the vertex buffer with offset=0. + m_vertexInput.m_vertexAttributes = layout.attributes; +} + +void Application::initShaderObjects() +{ + + // This function will read our shaders and create shader objects. + // Shaders are programs that run on the GPU. They are the core way for an application to control the GPU. + // GPUs tend to have a SIMD architecture, so shaders are designed to run in parallel on many threads. + // There are multiple ways to write shaders, but the most common way is to use a shading language. + // Traditional languages such as GLSL or HLSL are relatively popular. This tutorial will use Slang. + // Slang is a modern shading language hosted by Khronos, designed to modernize shader programming. + // + // Shader objects are the new way for Vulkan to interact with shaders. Similar to pipelines in Vulkan 1.0, they + // provide a way to bind shaders to the GPU, but they are more flexible and easier to use than pipelines. + + util::log_msg("[Init] Creating shader objects"); + + // We have three pairs of shaders, which is equivalent to three pipelines. + // The current function will create three shader groups, one for each pair of shaders. + // + // The objective of the tutorial is to showcase how an application can handle multiple shader groups with shader + // objects. Each draw call will store a reference to the shader objects it uses, which will be bound at run time if + // necessary. This is a basic strategy, focused on fundamentals rather than performance and scalability. + m_shaderObjects.m_albedoAndNormal = createShaderGroup(SceneData::albedoAndNormalShaderFilePair); + m_shaderObjects.m_albedo = createShaderGroup(SceneData::albedoShaderFilePair); + m_shaderObjects.m_solidColor = createShaderGroup(SceneData::solidColorShaderFilePair); +} + +Application::ShaderGroup Application::createShaderGroup(const ShaderFilePair& shaderFilePair) +{ + // This tutorial uses shader objects to manage its pipelines / shader groups. + // + // Vulkan 1.0 is built around the idea of pipelines (`VkPipeline`). + // When we create a pipeline, we need to specify the code for all shader stages and most graphics state upfront, + // then the driver compiles them into a single object. Pipelines have proved problematic for many applications: + // - Requiring all state during initialization is not very flexible, causing a huge number of PSOs. + // - Pipeline caching has proved insufficient for most big applications. + // - Creating and using `VkPipelines` requires a lot of boilerplate in Vulkan. + // + // This tutorial uses shader objects. + // `EXT_shader_object` provides a more flexible and forward-looking way to use shaders in Vulkan. + // - Vertex and fragment shaders can be created independently, and combined during command buffer recording. + // - We use dynamic state to specify graphics state. + // The shader object extension requires applications to specify the required dynamic state before rendering. + // This reduces PSOs since we do not need to specify it at initialization. + // - The API is more direct and reduces the amount of boilerplate needed to create and bind shaders. + // - It enables advanced use cases, by giving applications direct access to shader binaries for internal caching, + // and control over shader linking. + // - It simplifies the API and reduces boilerplate. + // + // A pipeline state object (PSO) is an informal term for the data that defines a pipeline. + // This includes all shaders used (vertex and fragment), fixed-function state, descriptor compatibility, etc. + // + // Pipelines describe the state, shaders, etc. that control operations on the GPU. + // This tutorial uses vertex and fragment shaders, which are part of the graphics pipeline. + // Beginners should understand the different parts of a graphics pipeline, including the fixed-function stages. + // Note: Geometry and tessellation shaders are not commonly used and can be ignored by most developers. + // + // Beginners should also be aware of other pipeline types, especially compute pipelines, since + // compute shaders are extremely popular for GPGPU and are becoming common in rendering use cases. + // Note: With shader objects, applications use a different abstraction and no longer create pipeline objects. + // Pipelines still exist as a concept, even if we no longer create a Vulkan object for them. + // + // With shader objects, we can create a shader group by creating the vertex and fragment shader objects + // independently. This significantly reduces boilerplate because we can avoid layouts and intermediate objects and + // directly use SPIR-V to create our shaders. We can then bind those shaders during command buffer recording. + // Note: Currently the tutorial has linking enabled, so shaders are not created independently. + // + // Most Vulkan tutorials would create a graphics pipeline here. + // This tutorial relies on forward-looking strategies like shader objects and dynamic state instead. + + // Path to read the SPIR-V and reflection data, generated during compilation. + const std::filesystem::path shaderDir = SIGGRAPH_SHADER_DIR; + + // Contains mappings from shader set/binding declarations to descriptor-heap mappings. + // The main idea is to link binding data, specified in Slang by `[[vk::binding(bindingId, setId)]]`, + // to descriptor heap offsets that select the correct descriptor. + std::vector mappings; + { + // We need to map shader resources to Vulkan descriptors. + // This lets the application tell Vulkan which resources a shader will use for each shader variable. + // We are using descriptor heaps, so we do not create descriptor sets or related layout objects. + // To identify shader variables, shaders can still use classic set/binding declarations, + // such as `[[vk::binding(0, 0)]] g_camera`. During shader creation, we attach a mapping that links those + // bindings to offsets inside the bound heap buffers. That is why this sample does not create descriptor set + // layouts, descriptor sets, or a pipeline layout. + // Note: Descriptor heaps are compatible with untyped pointers. This could allow applications to use an untyped + // shader model where shaders directly index descriptors in heap memory. + // This is more flexible and skips mapping creation, but it is more complex and advanced. + // This tutorial ignores that path and uses classic set/binding decorations instead. + + // We use reflection data to generate our shader bindings. + // Note: We are reading the reflection data from a JSON file at runtime. + // Normal applications would likely want to read this data at build time, usually using a script to + // generate C++ code with the mappings baked in. Most engines will also generate shader-specific structs + // like PackedVertex and the descriptor-heap push data structs from this reflection data at build time. + const std::array shaderReflectionPaths{ + shaderDir / std::format("{}.json", shaderFilePair.vertexName), + shaderDir / std::format("{}.json", shaderFilePair.fragmentName), + }; + + // This function will read the reflection data from the JSON files and collect all shader resource bindings. + // It will ensure that bindings are compatible between shaders, and that there are no duplicate bindings. + const std::unordered_map& shaderResourceBindings = + util::slang::collectShaderResourceBindings(shaderReflectionPaths); + + // Main function to create all mapping information. + mappings = buildShaderDescriptorMappings(shaderResourceBindings); + } + + // When creating shader objects, we use this to link shader bindings to descriptor heap mappings. + // Note: Mappings need to be set at shader creation time, + // since the GPU compiler needs to know the offsets of each descriptor in the heap. + // Note: We always create mappings, but they are ignored when using shader binaries rather than SPIR-V. + const vk::ShaderDescriptorSetAndBindingMappingInfoEXT mappingInfo{ + .mappingCount = util::safeCastToU32(mappings.size()), + .pMappings = mappings.data(), + }; + + // We need to use shader flags to enable descriptor heap and other options. + vk::ShaderCreateFlagsEXT shaderFlags = vk::ShaderCreateFlagBitsEXT::eDescriptorHeap; + + // Shader Object Linking: + // + // With shader objects, we control linking: shader objects can be created independently or linked together. + // Disabling linking lets us create each shader object independently. This can reduce the number of shader objects + // we need to create because it lets us reuse individual vertex or fragment shaders in different pairs, reducing + // the number of shader groups / PSOs. However, disabling linking prevents the driver from optimizing the + // vertex/fragment pair together, which can significantly reduce GPU performance. + // + // There is a trade-off between enabling shader object linking to improve GPU performance, + // and reusing shaders across different pipelines to reduce PSOs. + // Advanced applications can combine both approaches. First, create the unlinked shaders. Individual shaders are + // usually reused across multiple shader pairs, so they are likely to already be compiled, created, and ready for + // use. Then re-create the shader-pair objects with linking enabled for better GPU performance. + // + // Our tutorial has the variable enable_linking to control linking. + // We recommend that most applications enable linking for better GPU performance, + // since PSO creation usually becomes a problem mainly in larger applications with many shader combinations. + // + // Note: If we enable linking, the shaders in the shader pair have to be used together. + // When we enable the linking flag, all shaders created in the same call must be bound in the same command + // buffer before being used, so applications are not allowed to use the vertex shader with a different + // fragment shader, or vice versa. We can still use multiple bindShadersEXT calls to bind the shaders. + // Note: If linking is enabled, we need to send the pair of shaders to the driver at shader creation time. + // This enables link-time optimization (LTO) across both shaders to improve GPU performance. + constexpr bool enable_linking = true; + if constexpr (enable_linking) { + shaderFlags |= vk::ShaderCreateFlagBitsEXT::eLinkStage; + } + + constexpr std::size_t numShadersInPipeline = 2; // Vertex + fragment. + + // Store shader code until shader objects are created. vk::ShaderCreateInfoEXT points into these vectors. + // + // BinaryBuffer is just a wrapper around a byte vector. + // It adds an offset to an internal vector to handle alignment requirements. + // Note: In Vulkan, code used to create shader objects has alignment requirements. + std::vector currentShadersCode; + std::vector createInfos; + + // Small optimization: reserve space for the shader code and ShaderCreateInfoEXT vectors to avoid reallocations. + // + // ShaderCreateInfoEXT stores pointers into the inner byte buffers. + // These are pointers to BinaryBuffer::storage, which is a vector. + // Reallocating currentShadersCode can move BinaryBuffer objects, but moving a std::vector does not move its + // contents, so pointers into the inner buffers remain valid after the outer vector reallocates. + currentShadersCode.reserve(numShadersInPipeline); + createInfos.reserve(numShadersInPipeline); + + // Read SPIR-V code from disk. + // This helper reads binary SPIR-V data from disk and stores it in a byte vector. + // It checks that the SPIR-V file exists and that its size is a multiple of 4 bytes. + // The return byte vector has the alignment required to create the shader objects. + const std::filesystem::path vertexSpirvPath = shaderDir / shaderFilePair.vertexName; + const std::filesystem::path fragmentSpirvPath = shaderDir / shaderFilePair.fragmentName; + util::BinaryBuffer vertexSpirvWords = util::readSpirvFile(vertexSpirvPath); + util::BinaryBuffer fragmentSpirvWords = util::readSpirvFile(fragmentSpirvPath); + + //*********** + // Shader Binary Cache data start + //*********** + + // The tutorial implements a simple shader binary cache to store compiled shader binaries on disk. + // This is an advanced option and can be skipped on first read. + // + // Vulkan has a pipeline cache object to help reuse pipeline compilation work. + // This has proven insufficient for advanced use cases, since applications still have to manage their own pipeline + // cache data. PipelineCache can be serialized and reused, but it is opaque, so it is difficult for applications + // with large shader sets to manage, since it might require caching on top of PipelineCache. + // Shader objects provide a more flexible way to cache shaders, allowing direct access to the shader binaries. + // + // The importance of a proper shader binary cache greatly depends on the platform. Some platforms have a + // driver-internal or cloud shader cache that reduces the impact of an application shader cache. + // However, on some platforms and applications, a shader cache can be critical for fast app initialization. + // + // It is also important to avoid recreating the same shaders multiple times. + // Once an application reads a shader or shader pair and creates shader objects, + // it should avoid recreating the same shader objects. + constexpr bool enable_shader_binary_cache = true; + + // Path to store and read the optional binary shader cache. + const std::filesystem::path shaderBinaryCacheDir = shaderDir / "shaderBinaryCache"; + + // Wrapper for data we need to update in our cache. + struct ShaderCacheMiss { + std::filesystem::path cachePath; + std::uint32_t shaderIndex; + }; + + // Store shaders that miss the binary cache so their binaries can be saved after creating a shader pair. + // The list of cacheMisses has one entry for each shader we cannot find in the cache. After creating the shader + // objects, we use this list to write all missing shader binaries to the cache. + std::vector cacheMisses; + + // Key used to identify shader binaries in the cache. + // Each entry in the cache will have a unique key, so this variable needs to contain all relevant data. + std::uint64_t shaderPairCacheKey = 0; + + if (enable_shader_binary_cache) { + + // Keep an internal version number for cache keys and file formats. + // If we ever change how our cache works and need to invalidate keys, we should update this value. + // This will let us invalidate our current cache from the application side. + // Note: keeping an internal version is a good programming best practice. + constexpr std::uint32_t shaderBinaryCacheVersion = 1; + + // Calculate a hash for the shader binary cache. + // Our key is a hash that uses all relevant data for the cache entry. + // The final key will include mapping information, SPIR-V, linked shader and device properties. + // Note: This part of the key is the same for all groups, + // we could move it to device initialization but we recalculate it here for simplicity. + shaderPairCacheKey = util::combineHash( + // Shader-object binaries are driver-specific. + // The pair key hashes the shader binary UUID, version, etc. reported by the selected physical device. + shaderBinaryCacheVersion, std::span{m_shaderBinaryCacheProperties.m_shaderBinaryUUID}, + m_shaderBinaryCacheProperties.m_shaderBinaryVersion); + + if (enable_linking) { + // When creating linked shader objects, both shaders affect the compiled shader binary of each stage, + // so the code for both shaders must be part of the key. + // Note: For simplicity, the tutorial hashes the SPIR-V at runtime for each shader, + // but a real application would probably use a pre-calculated hash. + shaderPairCacheKey = util::combineHash(vertexSpirvWords.as_byte_span(), fragmentSpirvWords.as_byte_span(), + shaderPairCacheKey); + } + + // Add shader mapping data to the cache key. + { + // Descriptor heap mappings are part of shader-object creation, so they should be included in the key. + // We are storing the values and device limits that affect how the offsets in the heap are calculated. + // Layers and debugging tools can change device limits, so missing one of these values could make the cache + // reuse a binary that was compiled with different descriptor heap assumptions. + // We would need to update the shaderBinaryCacheVersion if we change how our mappings are created. + // This is a relatively naive implementation. We should include all values and decisions that affect + // buildShaderDescriptorMappings(). + shaderPairCacheKey = + util::combineHash(shaderPairCacheKey, m_descriptorHeaps.m_resourceHeap.m_minReservedRange, + m_descriptorHeaps.m_samplerHeap.m_minReservedRange, m_descriptorHeaps.m_cameraOffset, + m_descriptorHeaps.m_uniformBufferStride, m_descriptorHeaps.m_objectsOffset, + m_descriptorHeaps.m_pointLightOffset, m_descriptorHeaps.m_solidColorOffset, + m_descriptorHeaps.m_pushTextureOffset, m_descriptorHeaps.m_sampledImageStride, + m_descriptorHeaps.m_nearestSamplerOffset, m_descriptorHeaps.m_linearSamplerOffset); + } + } + + //*********** + // Shader Binary Cache data end + //*********** + + // Helper to initialize and add each stage shader create info. + const auto addShaderCreateInfo = [&cacheMisses, &createInfos, ¤tShadersCode, &mappingInfo, + shaderBinaryCacheDir, shaderFlags, shaderPairCacheKey]( + std::string_view shaderName, + // Note: shaderName is only used for logging purposes. + vk::ShaderStageFlagBits stage, std::span spirvBytes) { + static_cast(shaderPairCacheKey); + std::optional> shaderCode; + + vk::ShaderCodeTypeEXT codeType = vk::ShaderCodeTypeEXT::eSpirv; + + // Skipped when shader cache is disabled. + // Check if disk has a binary for this shader and we can skip compilation. + if (enable_shader_binary_cache) { + + // Generate the part of the binary cache key that identifies this shader. + // + // All common data for the pipeline was already stored in shaderPairCacheKey. + // Now we only need to identify the shader in the pipeline. + // If linking is disabled we store the hash of the SPIR-V bytes. + // If linking is enabled, all shaders affect the output, so their SPIR-V was added during initialization, + // and we only need to add the stage to the hash. + uint64_t shaderBinaryCacheKey = enable_linking ? util::combineHash(stage, shaderPairCacheKey) + : util::combineHash(spirvBytes, shaderPairCacheKey); + + // We are using the cache key as the name of a binary file containing the shader binary. + const std::filesystem::path shaderCachePath = + shaderBinaryCacheDir / std::format("{:016x}.bin", shaderBinaryCacheKey); + + // Check whether a non-empty cache file exists and read it. This tutorial trusts the cache file once found. + // We assume that if a cache file is read successfully, it contains the correct shader binary we want. + // + // This trusts the cache completely, which is not production-ready. + // Production code should handle corrupt files and concurrent readers/writers more defensively. + // + // Driver updates can invalidate the cache. We include driver properties like the UUID + // in the cache key to account for this, but this still depends on the cache key being complete. + // Note: This is a tutorial. For simplicity, we have not implemented a way to clean up the cache. + // Our cache will grow with more shaders and driver updates, a proper application would need a way to + // clean up invalid and old entries. + + // Vulkan requires shader binary buffers used to create shader objects to have this byte alignment. + std::size_t shaderBinaryAlignment = 16; + + // This wrapper will check if the file exists and read it. + // It returns an aligned vector and checks that the file is not empty. + std::optional cachedShaderBinary = + util::readBinaryFile(shaderCachePath, shaderBinaryAlignment); + + const bool hasShaderBinary = cachedShaderBinary.has_value(); + + // If a non-empty cache file was read, we have the shader binary in our cache. + if (hasShaderBinary) { + util::log_msg("[Init] Reading Cached Shader Binary: {}", shaderCachePath.string()); + + // We can use the binary read from the file to create the shader object. + // This shader binary data needs to live until shader creation, so we store it in a vector. + // Note: The span points into an inner vector. + // Moving a std::vector does not invalidate references to its contents. + // So we can safely reallocate the outer vector. + shaderCode = currentShadersCode.emplace_back(std::move(cachedShaderBinary.value())).as_byte_span(); + + // If we found an entry in the cache, we will use the binary, + // so we mark the current shader as binary rather than SPIR-V. + codeType = vk::ShaderCodeTypeEXT::eBinary; + } + else { + // If the shader is not in our cache, record the miss so it can be saved after shader creation. + cacheMisses.push_back(ShaderCacheMiss{ + .cachePath = shaderCachePath, + .shaderIndex = util::safeCastToU32(createInfos.size()), + }); + } + } + + // shaderCode only has a value when the shader binary cache has a hit. + // If the shader is not in the cache, we create the shader object as a SPIR-V shader. + if (!shaderCode.has_value()) { + util::log_msg("[Init] Reading SPIR-V Shader: {}", shaderName); + + // Light validation of the SPIR-V bytes when reading the file. + // We know the file is not empty and we checked it has a valid size and alignment (a multiple of 4 bytes). + util::require(!spirvBytes.empty(), "Shader key generation must load SPIR-V before shader creation"); + + // We can assume codeType is SPIR-V. The only way we modify codeType is after a shader cache hit, + // and we will not enter this branch in that case because shaderCode has a value. + util::require(codeType == vk::ShaderCodeTypeEXT::eSpirv, "SPIR-V shader code must use eSpirv code type"); + + // We use the SPIR-V data to create our shaders. + shaderCode = spirvBytes; + } + + // Check that the input code is correctly aligned. + { + // When creating shader objects, the buffers containing our shader code have alignment requirements. + // The spec defines the required alignment depending on whether they contain SPIR-V or binary data. + // We are currently using BinaryBuffer, which handles these alignment requirements for us. + util::require(reinterpret_cast(shaderCode->data()) == + util::alignUp(reinterpret_cast(shaderCode->data()), + codeType == vk::ShaderCodeTypeEXT::eSpirv ? size_t(4) : size_t(16)), + "When creating a shader object, code must be aligned to 16 bytes for binary objects and 4 " + "bytes for SPIR-V"); + } + + // Add one shader create info for each stage in the pipeline. + // This is necessary for linking, since all shaders in the pipeline have to be created together. + // For simplicity, the tutorial uses the same strategy when linking is disabled. + // This might create the same shader object multiple times when linking is disabled, + // and should be avoided by a real application. + createInfos.emplace_back(vk::ShaderCreateInfoEXT{ + // Note: Mappings are ignored if code type is binary. We could still set them, but they would be ignored. + .pNext = (codeType == vk::ShaderCodeTypeEXT::eSpirv) ? &mappingInfo : nullptr, + .flags = shaderFlags, + .stage = stage, + // We need to specify the next stage in our pipeline. + // The tutorial avoids tessellation and geometry shaders, so it is safe to assume that the next stage + // after a vertex shader is a fragment shader. + // We only support vertex and fragment shaders, and there is no next stage after a fragment shader. + .nextStage = (stage == vk::ShaderStageFlagBits::eVertex) ? vk::ShaderStageFlagBits::eFragment + : vk::ShaderStageFlagBits(0), + // The only necessary change between creating a shader object using SPIR-V or binary data is codeType. + // Our application stores either the SPIR-V or binary data in shaderCode. + // Note: We can still specify mappings in pNext, but they will be ignored by binary shaders. + // Note: shaderCode will store SPIR-V or binary data. + .codeType = codeType, + .codeSize = shaderCode->size(), + .pCode = shaderCode->data(), + // Slang reflection reports source function names. The generated SPIR-V entry point is "main". + .pName = "main", + }); + }; + + // The tutorial creates shader objects using linking. + // We need to specify all shaders in the pipeline in the same call. + // When linking, shader objects require us to create both the vertex and fragment shaders in the same call. + addShaderCreateInfo(shaderFilePair.vertexName, vk::ShaderStageFlagBits::eVertex, vertexSpirvWords.as_byte_span()); + addShaderCreateInfo(shaderFilePair.fragmentName, vk::ShaderStageFlagBits::eFragment, + fragmentSpirvWords.as_byte_span()); + + util::log_msg("[Init] Create ShaderObjects: {} + {}", shaderFilePair.vertexName, shaderFilePair.fragmentName); + + // Check that all linked shaders use the same code type: all must be binary or SPIR-V. + if (enable_linking) { + // When linking shaders using shader objects, all shaders we send in the same call need to have the same + // codeType. They all have to be either SPIR-V or binary; we are not allowed to combine them. + // If our cache detects a hit for the vertex shader, but not for the fragment shader, this check throws before + // we try to create shader objects with invalid API usage. + // Under normal conditions, the vertex and fragment shaders cannot have mixed code types because we always save + // both shaders to the cache. However, file-write failures or users deleting cache files can cause this. + // Printing an error and failing is acceptable for a tutorial, but a real application would probably want to + // update the cache and try to create both shaders using SPIR-V. + util::require( + cacheMisses.empty() || (createInfos.size() == cacheMisses.size()), + std::format("Error in shader cache for shaders {} and {}. All shaders should be binary or SPIR-V. " + "Consider deleting the shader cache.", + shaderFilePair.vertexName, shaderFilePair.fragmentName)); + } + + // Linking requires creating all shaders in the pipeline in the same call. + // For simplicity, always create both shaders in the same call. + auto result = m_logicalDevice.createShadersEXTUnique(createInfos); + + // Ensure both shaders have been created correctly. + // + // Note: the tutorial is treating eIncompatibleShaderBinaryEXT as fatal. + // A proper application would detect if the driver failed to generate shader objects from the shader binaries in the + // cache, and if that happens it would fall back to trying to create the shaders using SPIR-V. + util::require(result.has_value(), + std::format("Failed to create shader objects for {} and {}: {}", shaderFilePair.vertexName, + shaderFilePair.fragmentName, vk::to_string(result.result))); + + // Get all shader objects. + std::vector shaders = std::move(result.value); + util::require(shaders.size() == 2, "Expected linked vertex and fragment shader objects"); + + // Shader binary cache write. + if (enable_shader_binary_cache) { + + // If the shader binary cache is enabled, update it. + // This loop iterates through shaders that we did not find in the cache and saves their binaries to disk. + for (const ShaderCacheMiss& cacheMiss : cacheMisses) { + + // Shader objects make managing a shader cache straightforward. + // We can easily retrieve the compiled binary from a shader by calling getShaderBinaryDataEXT. + // Then we can use this data when creating a new shader object. + // This offers more direct control and an easier method for managing the cache than PipelineCache. + const std::vector shaderBinary = + m_logicalDevice.getShaderBinaryDataEXT(*shaders[cacheMiss.shaderIndex]); + + // Save it to disk for future runs. This simple tutorial assumes the file write succeeds. + // The cache key was already calculated to check if the file is in the cache. + // Remember that it includes the shader SPIR-V to make it unique per shader, and the UUID/version so driver + // updates use different cache files, alongside all other relevant data. + util::writeBinaryFile(cacheMiss.cachePath, shaderBinary); + + util::log_msg("[Init] Writing Shader Binary Cache: {}", cacheMiss.cachePath.filename().string()); + } + } + + ShaderGroup shaderGroup{ + .m_vertex = std::move(shaders[0]), + .m_fragment = std::move(shaders[1]), + }; + + // Set debug names for the shaders. + { + const std::string vertexDebugName = + std::format("Vertex Shader ({} + {})", shaderFilePair.vertexName, shaderFilePair.fragmentName); + setDebugName(*shaderGroup.m_vertex, vk::ObjectType::eShaderEXT, vertexDebugName); + + const std::string fragmentDebugName = + std::format("Fragment Shader ({} + {})", shaderFilePair.vertexName, shaderFilePair.fragmentName); + setDebugName(*shaderGroup.m_fragment, vk::ObjectType::eShaderEXT, fragmentDebugName); + } + return shaderGroup; +} + +//**************** +// Runtime loop +//**************** + +void Application::mainLoop() +{ + // This is the main loop function of the application. + // + // Most games have a similar function, usually called a main loop or rendering loop. + // This function starts and runs the main loop that the application will run. + // + // The common workflow for a game is: + // - Initialization: create rendering and non-rendering resources. + // - Main loop: run each frame to handle input, update game logic, and generate new rendering commands. + // - Resource cleanup: destroy and free all game resources. + // + // This function will start a loop that runs until the window is closed. + // We use this loop each frame to: + // - Handle keyboard input. + // - Update the camera. + // - Issue new rendering commands. + // + // Real games follow a more complex but similar approach. + // Our tutorial does not have logic, a physics system, or animations, but they are usually handled here. + // Note: real games should consider separating logic update times and rendering times. + // For example, you might want to render at 60 FPS but update your game simulation every 25 ms. + // You might also want to consider different threads for rendering and logic updates. + // Note: Game engine architecture is an extremely complex topic. + // You might want to check object hierarchies (tree/node systems) and Entity Component Systems (ECS). + + // Measure current time to calculate delta time for camera movement and FPS logging. + // It is a best practice to use time deltas to update logic systems, like camera movement. + // Proper applications might also use this for animation, physics, or game logic. + // Note: GLFW returns the time in seconds, and this tutorial uses seconds for all times. + double previousTime = glfwGetTime(); + double lastFpsLogTime = previousTime; + + // We print a simple FPS message every framesPerLog frames. + constexpr uint32_t framesPerLog = 20; + uint32_t remainingFramesLog = framesPerLog; + + // We have a CLI option to exit the application after a certain number of frames. + // Log it at the beginning of the run so it is easy to see in the logs. + // Logs and CLI flags can greatly help with automated testing and debugging. + // Consider CLI flags like: --save_image_to_disk, --disable_cache, etc. + if (m_remainingFrameLimit > 0) { + util::log_msg("[Run] Frame limit: {}", m_remainingFrameLimit); + } + + // Loop until window is closed. + // This is the main loop. The application will run until the user closes the window or presses escape. + while (glfwWindowShouldClose(m_window) == GLFW_FALSE) { + + // Update CPU time. We use GLFW to get the current time. + // We store last frame time and calculate delta time for camera movement and FPS logging. + const double currentTime = glfwGetTime(); + const float deltaSeconds = static_cast(currentTime - previousTime); + + // Small validation that frame time is non-negative. + util::require(deltaSeconds >= 0.0f, std::format("Invalid delta time {} between frames", deltaSeconds)); + + // Next iteration will use the current time as previous time. + previousTime = currentTime; + + // Simple FPS logging using FPS counter and delta times. + if (--remainingFramesLog == 0) { + // Seconds since last frame log. + const float elapsedSeconds = static_cast(currentTime - lastFpsLogTime); + + // Check to avoid division by 0. + // This should not happen unless the timer did not advance while rendering frames. + util::require(elapsedSeconds > 0.0f, std::format("Invalid elapsed seconds {}", elapsedSeconds)); + + const float fps = framesPerLog / elapsedSeconds; + util::log_msg("FPS: {}", fps); + remainingFramesLog = framesPerLog; + lastFpsLogTime = currentTime; + } + + // Poll events. Inputs are handled in updateCamera() using GLFW. + glfwPollEvents(); + + // Detect if escape key is pressed and close the window if it is. + if (glfwGetKey(m_window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { + glfwSetWindowShouldClose(m_window, GLFW_TRUE); + break; + } + + // We use GLFW to read cached input, detecting key presses to update the camera position and orientation. + // We use deltaSeconds to update the camera position and orientation independent of the frame rate. + updateCamera(deltaSeconds); + + // Main Vulkan code. + // This creates and submits all commands to render the current frame. + renderFrame(); + + // Simple CLI option to finish after a certain number of frames, useful for automated testing. + if (m_remainingFrameLimit > 0) { + --m_remainingFrameLimit; + util::log_msg("Remaining frames: {}", m_remainingFrameLimit); + if (m_remainingFrameLimit == 0) { + glfwSetWindowShouldClose(m_window, GLFW_TRUE); + } + } + } +} + +void Application::updateCamera(float deltaSeconds) +{ + // This function uses GLFW to detect pressed keys and use the keyboard to move the camera. + // We are only updating the data we have in the CPU. Camera data is later uploaded to the GPU. + // Note: For tutorial simplicity we do not handle mouse input. + + // Limit delta seconds to avoid jumps when the frame stutters. + deltaSeconds = std::min(deltaSeconds, 0.3f); + + // Rotation using arrow keys. + { + // Rotate camera using arrow keys. + // Mouse control is not implemented in this tutorial, but it would be simple to implement using GLFW. + constexpr float cameraLookSpeed = 1.6F; + const float lookStep = cameraLookSpeed * deltaSeconds; + // Main loop calls glfwPollEvents to poll and process pending events. + // We use glfwGetKey to read GLFW cached key state. + if (glfwGetKey(m_window, GLFW_KEY_LEFT) == GLFW_PRESS) { + m_cameraYaw -= lookStep; + } + if (glfwGetKey(m_window, GLFW_KEY_RIGHT) == GLFW_PRESS) { + m_cameraYaw += lookStep; + } + if (glfwGetKey(m_window, GLFW_KEY_UP) == GLFW_PRESS) { + m_cameraPitch += lookStep; + } + if (glfwGetKey(m_window, GLFW_KEY_DOWN) == GLFW_PRESS) { + m_cameraPitch -= lookStep; + } + // calculateRight uses a cross product. Avoid looking straight up/down to avoid problems. + m_cameraPitch = std::clamp(m_cameraPitch, -cameraPitchLimit, cameraPitchLimit); + } + + // Movement using WASD and QE. + { + // We are implementing a free camera movement for our game. + // Classic WASD movement controls: WS for forward/backward and AD for left/right. + // E moves up; Q moves down. + // + // We are using GLFW for mouse and keyboard input. + // This is a simple cross-platform library that can handle input. We can also use SDL as an alternative. + const glm::vec3 forward = util::math::calculateForward(m_cameraPitch, m_cameraYaw); + const glm::vec3 worldUp{0.0F, 1.0F, 0.0F}; + const glm::vec3 right = util::math::calculateRight(forward, worldUp); + + glm::vec3 movement{0.0F}; + if (glfwGetKey(m_window, GLFW_KEY_W) == GLFW_PRESS) { + movement += forward; + } + if (glfwGetKey(m_window, GLFW_KEY_S) == GLFW_PRESS) { + movement -= forward; + } + if (glfwGetKey(m_window, GLFW_KEY_D) == GLFW_PRESS) { + movement += right; + } + if (glfwGetKey(m_window, GLFW_KEY_A) == GLFW_PRESS) { + movement -= right; + } + if (glfwGetKey(m_window, GLFW_KEY_E) == GLFW_PRESS) { + movement += worldUp; + } + if (glfwGetKey(m_window, GLFW_KEY_Q) == GLFW_PRESS) { + movement -= worldUp; + } + + if (glm::length(movement) > 0.0F) { + constexpr float cameraMoveSpeed = 4.0F; + m_cameraPos += glm::normalize(movement) * cameraMoveSpeed * deltaSeconds; + } + } +} + +void Application::renderFrame() +{ + // This function is executed during the main loop. + // This function will: + // - Update data on the CPU (update data in the camera buffer). + // - Record commands to render the current frame. + // - Submit the command buffer so it is executed on the GPU. + + // Summary of synchronization for the frame: + // - Fence: lets the CPU wait before reusing a frame slot. + // - Semaphores: order GPU-side acquire, render, and present work. + // - Events: allow more granular synchronization, but are unused in this tutorial. + // - Barriers: order image layout/access changes inside the recorded command buffer. + + // The helper command buffer is used to upload data to the GPU during initialization. + // It should not be active during rendering. + util::require(!m_helperCommandBuffer.m_active, "Helper command buffer must not be active while rendering"); + + // Select the current frame-in-flight resources. + // + // Our render uses multiple frames in flight. This allows the application to start working on the next frame while + // the previous frame is still in flight. + // Frames in flight make us duplicate some resources, but help ensure correct utilization. + const std::size_t frameIndex = m_currentFrameInFlight; + FrameInFlightResources& frame = m_framesInFlight[frameIndex]; + + // This ensures this frame's resources are available. + waitForFrameResources(frame); + + // We use frame resources since the swapchain signals frame.m_imageAvailableSemaphore when the image is available. + // It is important that frame and swapchain resources are kept independent. + const std::uint32_t swapchainImageIndex = acquireSwapchainImage(frame); + + // Reset and begin the main command buffer so we can start recording commands. + // Note: We need to call this before we start recording work in our command buffer. + // In our tutorial we could call this after uploadCameraData, + // but a real application is likely to use staging buffers with commands to update GPU resources. + startRecordingCommandBuffer(frame); + + // Use CPU data to update GPU data. + // We only have a camera buffer, but this usually includes animations, streaming new objects, etc. + // + // Because we are using host memory, we do not need to record commands to update resources. + // But most apps will have a more optimized approach to update resources using commands. + // Note: we still wait for a fence and submit work after writing to avoid race conditions. + uploadCameraData(frame); + + // Record commands used to render. + // This records commands on the CPU that are later executed on the GPU. + // This only records rendering commands. + recordRenderingCommandBuffer(frame, util::safeCastToU32(frameIndex), swapchainImageIndex); + + // Submit the command buffer with rendering commands to the GPU so that it can be executed. + // This will mark the command buffers as ended before submission. + // We will also present the image to the swapchain and handle inter-frame synchronization. + finishAndSubmitMainCommandBuffer(frame, swapchainImageIndex); + + // Advance through the small ring of frame resources. + m_currentFrameInFlight = + util::safeCastToU32((static_cast(m_currentFrameInFlight) + 1U) % m_framesInFlight.size()); +} + +void Application::waitForFrameResources(FrameInFlightResources& frame) +{ + // This ensures that we can reuse the per-frame resources. + // This fence protects frame resources rather than swapchain data, allows the application to record new commands, + // and ensures that intermediate resources like depth images or the camera buffer are safe to use. + + // Wait until this frame slot's previous GPU work is done before reusing its resources. + // We cannot write previous resources unless we are done using them. + // Note: We use a fence because the CPU needs to wait for the GPU. CPU waits to reuse command buffer and other frame + // resources until the GPU finishes using them. + const std::array frameFences{*frame.m_inFlightFence}; + + const bool waitAll = true; + const uint64_t timeout = UINT64_MAX; + + const vk::Result waitResult = m_logicalDevice.waitForFences(frameFences, waitAll, timeout); + util::require(waitResult == vk::Result::eSuccess, "Failed to wait for frame fence"); +} + +std::uint32_t Application::acquireSwapchainImage(FrameInFlightResources& frame) +{ + // This function requests a new swapchain image so that we can use it for rendering. + + // Ask the swapchain for the next image index. + // The swapchain image is not immediately available, so we need synchronization. We use imageAvailableSemaphore as + // an acquire semaphore. The imageAvailableSemaphore is signaled when the presentation engine has released the + // image and the image is available for application use. In our application, we use this semaphore in the graphics + // queue to wait before rendering to the image (we render directly to the swapchain image). + const auto acquireResult = m_logicalDevice.acquireNextImageKHR( + vk::SwapchainKHR{m_vkbData.m_swapchain.swapchain}, UINT64_MAX, *frame.m_imageAvailableSemaphore, nullptr); + + // Tutorial simplifies swapchain management, ignoring most use cases. + { + // Tutorial skips recreating the swapchain, but a real app should properly deal with this. + if (acquireResult.result == vk::Result::eSuboptimalKHR) { + util::log_msg("Ignoring swapchain suboptimal: " + "Swapchain can still be presented but should be recreated soon."); + } + else { + util::require(acquireResult.result != vk::Result::eErrorOutOfDateKHR, + "Swapchain is out of date. Resize omitted from the tutorial."); + util::require( + acquireResult.result == vk::Result::eSuccess, + std::format("Failed to acquire swapchain image. Result {}\n", vk::to_string(acquireResult.result))); + } + } + const std::uint32_t swapchainImageIndex = acquireResult.value; + SwapchainImageResources& swapchainImage = m_swapchainImages[swapchainImageIndex]; + + // finishAndSubmitMainCommandBuffer describes the real semaphore wait during submission. + // This internal ImageState update only copies the wait stage into our tracked source state, + // so the next image barrier describes the dependency from image acquisition to the first color attachment write. + // This makes our dependency tracker consider the swapchain acquire semaphore wait when creating barriers. + // We are not changing the layout here. + swapchainImage.m_state.m_stageMask |= swapchainAcquireWaitStage; + + return swapchainImageIndex; +} + +void Application::uploadCameraData(FrameInFlightResources& frame) +{ + // Update camera data on the GPU. + // This updates the version of the camera data on the GPU. + // Note: We have one camera resource per frame in flight, so we will not overwrite the camera data. + + // Ensure we do not have division by 0. + util::require(m_swapchainExtent.height > 0, "Swapchain height must be non-zero before updating camera"); + + // CPU has the data needed to compute the camera matrix. + // We compute the camera matrix and send it to the GPU. + const float aspectRatio = + static_cast(m_swapchainExtent.width) / static_cast(m_swapchainExtent.height); + const CameraData camera{ + .viewProjection = util::math::calculateViewProjection(m_cameraPitch, m_cameraYaw, m_cameraPos, aspectRatio), + .cameraPosition = glm::vec4{m_cameraPos, 1.0F}}; + + // We will directly update the data in the camera buffer on the CPU, and then make it available to the GPU. + // Note: We write from the CPU after GPU reads and before submission to ensure synchronization. + // Note: Camera data is a host-coherent buffer, so we do not need to flush or invalidate. + // Note: A proper application would probably want a staging buffer to keep the shader data device local. + uploadBuffer(frame.m_camera, std::as_bytes(std::span{&camera, std::size_t{1}})); +} + +void Application::startRecordingCommandBuffer(FrameInFlightResources& frame) +{ + // Prepare a command buffer to start recording to it. + + // Reset command buffer before we start recording to it. + frame.m_commandBuffer.reset(); + + // Begin recording commands. This indicates we will start recording work. + frame.m_commandBuffer.begin(vk::CommandBufferBeginInfo{}); +} + +void Application::recordRenderingCommandBuffer(FrameInFlightResources& frame, std::uint32_t frameIndex, + std::uint32_t swapchainImageIndex) +{ + // This function creates the commands needed to render one frame. + // This function only handles rendering, resource management is handled by other functions. + + util::require(frameIndex < m_framesInFlight.size(), "Missing frame index"); + + // Get the resources associated with the swapchain image we use for rendering. + util::require(swapchainImageIndex < m_swapchainImages.size(), "Missing swapchain image"); + SwapchainImageResources& swapchainImage = m_swapchainImages[swapchainImageIndex]; + + // Store aliases to the frame resources we will use. + const GpuViewImage& depthImage = frame.m_depthImage; + const vk::CommandBuffer commandBuffer = frame.m_commandBuffer; + + // Add debug labels to identify the current frame. + // Debug labels are a key part of debugging and profiling applications. They interact well with tools like + // RenderDoc, allowing developers to identify which part of the frame is being analyzed. + const std::string frameDebugLabel = + std::format("Rendering Frame {} SwapchainImage {}", frameIndex, swapchainImageIndex); + beginDebugLabel(commandBuffer, frameDebugLabel, debugData.frameColor); + + // We are setting eAttachmentOptimal for the depth and color attachments. + // With synchronization2 enabled, we can use a generic attachment optimal layout + // instead of specialized layouts for both color and depth attachments. + // + // If unified image layouts were enabled, we could simplify this further, since the eGeneral layout + // could be used in place of most specialized layouts, with no expected performance loss. + // The current tutorial is too simple to benefit from unified layouts, but the extension + // can simplify barriers and reduce layout transitions for images with multiple usages. + // Note: unified image layouts do not remove the need to transition to/from present, + // and undefined remains useful for discards. + + // Transition from "present/undefined" into a layout valid for color attachment writes. + // In the tutorial, swapchain images move between rendering and presentation every frame, + // unlike depth images which stay in the depth-attachment layout after setup. + { + const ImageState attachmentState{ + .m_layout = colorAttachmentLayout, + .m_stageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .m_accessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .m_aspectMask = vk::ImageAspectFlagBits::eColor, + }; + transitionImage(commandBuffer, swapchainImage.m_image, swapchainImage.m_state, attachmentState, + CreateImageSubresourceRange()); + } + + // Record in the command buffer that rendering begins. + { + // We use beginRendering to indicate we can start doing draw calls. This is a command from dynamic rendering. + // With dynamic rendering, we no longer need to create render pass objects and set subpasses. Instead, we can + // set the information needed to start rendering in vk::RenderingInfo and call draw commands directly. + // This significantly reduces boilerplate and helps to make applications more dynamic. + // + // Note: Tile Based Rendering (TBR) GPUs can benefit from dynamic rendering local read, + // a Vulkan extension that allows subpass like local memory access with dynamic rendering. + // This tutorial does not use local read. We recommend reading about it when targeting mobile. + + // In order to begin rendering we need to set up our attachments. + // Attachments are image views connected to framebuffer operations during rendering. + // There are multiple types of attachments in Vulkan: + // color attachments: Store the outputs of the fragment shader. + // depth attachment: Stores the depth, used during depth testing to decide which fragments we will overwrite. + // stencil attachment: Stencil values work like a mask to discard fragments. Unused in this tutorial. + + // Set the clear values for the attachment in our application. + // Clear the swapchain color image and depth attachment before drawing the scene. + vk::ClearValue colorClearValue{}; + // Note: workaround to initialize a union. + colorClearValue.color.float32 = std::array{0.55F, 0.35F, 0.35F, 1.0F}; // Greyish pink for sky/background. + vk::ClearValue depthClearValue{}; + // Note: workaround to initialize a union + depthClearValue.depthStencil.depth = 1.0F; + + const std::array colorAttachments = {vk::RenderingAttachmentInfo{ + .imageView = *swapchainImage.m_imageView, // We render directly to the swapchain. + .imageLayout = colorAttachmentLayout, + .resolveMode = vk::ResolveModeFlagBits::eNone, + .resolveImageView = nullptr, + .resolveImageLayout = vk::ImageLayout::eUndefined, + .loadOp = vk::AttachmentLoadOp::eClear, // Clear before drawing. + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = colorClearValue, + }}; + + const vk::RenderingAttachmentInfo depthAttachment{ + .imageView = *depthImage.m_imageView, // Use per-frame depth image. + .imageLayout = depthAttachmentLayout, + // Clear depth attachment before rendering. + .loadOp = vk::AttachmentLoadOp::eClear, + // We do not use the depth attachment later. + // We only use it for depth testing during the pass, so we can discard it. + // Next pass will clear the current value, so we do not have to store it. + .storeOp = vk::AttachmentStoreOp::eDontCare, + .clearValue = depthClearValue, + }; + + util::require(m_swapchainExtent.height > 0 && m_swapchainExtent.width > 0, + "Render area dimensions must be greater than 0"); + + const vk::RenderingInfo renderingInfo{ + .renderArea = // Area to render. + vk::Rect2D{ + .offset = vk::Offset2D{.x = 0, .y = 0}, + .extent = m_swapchainExtent, + }, + .layerCount = 1, // This is commonly 1 except for VR or other layered rendering. + .colorAttachmentCount = util::safeCastToU32(colorAttachments.size()), + // Fragment shaders can have multiple outputs, so Vulkan supports multiple color attachments. + .pColorAttachments = colorAttachments.data(), + // We always have a single depth attachment. + // In Vulkan, a render pass has a single depth and a single stencil attachment. + .pDepthAttachment = &depthAttachment, + }; + + // Start rendering. + // This is equivalent to beginning a new dynamic rendering render pass instance, + // but a lot easier since we do not have to create all the intermediate objects. + commandBuffer.beginRendering(renderingInfo); + } + + // Set dynamic state. + { + // We are using dynamic state to set the graphics state of our application. + // In a graphics pipeline, some stages execute in fixed-function hardware and cannot be programmed; + // however, most of this state can be configured using graphics state. + // Traditionally, Vulkan expects this state to be given upfront when creating a pipeline. This has proven + // difficult for applications. Moreover, having the state as part of the pipeline has created an explosion of + // PSOs, where small state changes require a full pipeline recompilation. + // + // Dynamic state allows us to record part of the state during command recording, rather than during resource + // initialization. This delay helps applications manage resource creation. Removing the state from caching + // reduces the number of PSOs applications need to manage. + // + // Dynamic state can greatly help applications, so Vulkan has created new extensions to allow implementations to + // mark more state as supporting dynamic state. + // + // With shader objects, we no longer have pipelines, so all required graphics state has to be set using dynamic + // state as in our application. + // + // Note: dynamic state can still be used with pipelines. When creating the pipeline, the state is marked as + // dynamic. This will allow us to set the state during command recording rather than pipeline creation. + beginDebugLabel(commandBuffer, "Set Dynamic Graphics State", debugData.renderColor); + + // With dynamic state, we just need to record commands in our command buffer setting all relevant state. + + // Set viewport using dynamic state. + { + // The viewport maps normalized device coordinates to pixels in the framebuffer. + // We render to the entire swapchain image. + const vk::Viewport viewport{ + .x = 0.0F, + .y = 0.0F, + .width = static_cast(m_swapchainExtent.width), + .height = static_cast(m_swapchainExtent.height), + .minDepth = 0.0F, + .maxDepth = 1.0F, + }; + commandBuffer.setViewportWithCountEXT(viewport); + } + + // Set the scissor using dynamic state. + { + // The scissor controls which pixels are allowed to be written. + // Pixels outside of the scissor rectangle are discarded. + // We render to the entire swapchain image. + const vk::Rect2D scissor{ + .offset = vk::Offset2D{.x = 0, .y = 0}, + .extent = m_swapchainExtent, + }; + commandBuffer.setScissorWithCountEXT(scissor); + } + + commandBuffer.setPrimitiveTopologyEXT(vk::PrimitiveTopology::eTriangleList); + commandBuffer.setPrimitiveRestartEnableEXT(vk::False); + + commandBuffer.setRasterizerDiscardEnableEXT(vk::False); + commandBuffer.setDepthClampEnableEXT(vk::False); + commandBuffer.setPolygonModeEXT(vk::PolygonMode::eFill); + + commandBuffer.setCullModeEXT(vk::CullModeFlagBits::eNone); + commandBuffer.setFrontFaceEXT(vk::FrontFace::eCounterClockwise); + commandBuffer.setDepthBiasEnableEXT(vk::False); + + commandBuffer.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + { + const std::array sampleMasks{vk::SampleMask{0xFFFFFFFFU}}; + + commandBuffer.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMasks); + } + commandBuffer.setAlphaToCoverageEnableEXT(vk::False); + commandBuffer.setAlphaToOneEnableEXT(vk::False); + + commandBuffer.setLogicOpEnableEXT(vk::False); + { + const std::array colorBlendEnables{vk::False}; + commandBuffer.setColorBlendEnableEXT(0, colorBlendEnables); + } + { + const std::array colorWriteMasks{ + vk::ColorComponentFlags{vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA}}; + commandBuffer.setColorWriteMaskEXT(0, colorWriteMasks); + } + + commandBuffer.setDepthTestEnableEXT(vk::True); + commandBuffer.setDepthWriteEnableEXT(vk::True); + // Depth buffer is cleared to 1.0, so we keep fragments with a smaller depth value. + commandBuffer.setDepthCompareOpEXT(vk::CompareOp::eLess); + commandBuffer.setDepthBoundsTestEnableEXT(vk::False); + commandBuffer.setStencilTestEnableEXT(vk::False); + + // End Set Dynamic Graphics State. + endDebugLabel(commandBuffer); + } + + // Bind vertex input using dynamic state. + { + // We are using dynamic state to select the format of our vertex input. + // Usually, vertex input depends on the vertex shader, and might change per-draw. + // This sample has one vertex layout, so this is enough. + VertexInput& vertexInput = m_vertexInput; + commandBuffer.setVertexInputEXT(vertexInput.m_vertexBindings, vertexInput.m_vertexAttributes); + } + + // Validate descriptor heaps are ready. + { + util::require(m_descriptorHeaps.m_resourceHeap.m_buffer.m_addressGPU != 0, + "Resource heap must have a GPU address"); + util::require(m_descriptorHeaps.m_samplerHeap.m_buffer.m_addressGPU != 0, + "Sampler heap must have a GPU address"); + } + + // Bind descriptor heap buffers. + { + + // The shader mappings interpret heap bytes as descriptors. + // Now we need to bind our heaps so that we have a buffer from which to read these descriptors. + beginDebugLabel(commandBuffer, "Bind Descriptor Heaps", debugData.setupColor); + + // Helper to bind a heap. Allows us to reuse code to bind the resource and sampler heap. + auto createBindInfo = [](const DescriptorHeapResources::DescriptorHeapData& descriptorHeapData) { + return vk::BindHeapInfoEXT{ + .heapRange = + vk::DeviceAddressRangeEXT{ + // We do not bind the entire buffer. We have bindOffset as padding to ensure GPU alignment. + .address = descriptorHeapData.m_buffer.m_addressGPU + descriptorHeapData.m_bindOffset, + .size = descriptorHeapData.m_rangeSize, + }, + // Reserved range is a space reserved for implementation internal use. + // The application reserves this space and cannot use it while the heap is bound. + // Offset is 0 because this sample places the required reserved range at the start of the heap. + // The size in the tutorial is the queried minimum reserved range required on current hardware. + .reservedRangeOffset = 0, + .reservedRangeSize = descriptorHeapData.m_minReservedRange, + }; + }; + + // Create the bind info of both heaps. + const vk::BindHeapInfoEXT resourceHeapInfo = createBindInfo(m_descriptorHeaps.m_resourceHeap); + const vk::BindHeapInfoEXT samplerHeapInfo = createBindInfo(m_descriptorHeaps.m_samplerHeap); + + // The application binds the sampler and resource heap to the command buffer. + // Note: Switching heaps is expensive, for good performance applications are expected to bind the same sampler + // and resource heap during the entire application lifetime. + commandBuffer.bindResourceHeapEXT(resourceHeapInfo); + commandBuffer.bindSamplerHeapEXT(samplerHeapInfo); + + // End Bind Descriptor Heaps. + endDebugLabel(commandBuffer); + } + + // Helper function called during the object loop to bind the shader objects. + const auto bindShaderGroup = [commandBuffer](const ShaderGroup& shaderGroup) { + beginDebugLabel(commandBuffer, "Bind Shader Objects", debugData.setupColor); + + // We bind all active shaders in a single call. + const std::array stages{vk::ShaderStageFlagBits::eVertex, vk::ShaderStageFlagBits::eFragment}; + const std::array shaders{*shaderGroup.m_vertex, *shaderGroup.m_fragment}; + commandBuffer.bindShadersEXT(stages, shaders); + + // End Bind Shader Objects. + endDebugLabel(commandBuffer); + }; + + // Helper function to push data. + const auto pushDescriptorHeapShaderData = [maxPushDataSize = m_descriptorHeaps.m_maxPushDataSize, + commandBuffer](const auto& pushData) { + // This function uses push data to record some data that can be read by the GPU. + // Descriptor heaps offer push data, allowing the application to record a binary blob into push-data command + // state that GPU shaders will read. + // This can be used to replace push constants, but it has some significant advantages. + // It can be used to communicate some small data to the GPU that changes on each draw call. + // Our tutorial uses it to select the texture indices, but it could be used to send arbitrary data. + + // Offset of the GPU push-data region we will update. + // We are updating the entire copy on the GPU, so we set it to 0. + constexpr std::uint32_t pushDataOffset = 0; + + constexpr std::uint32_t pushDataSize = sizeof(pushData); + + // Small data validation. + { + // Ensure the push data we are updating does not exceed device limits. + // In a proper application, this check should be moved to initialization rather than recording. + util::require(pushDataOffset <= maxPushDataSize && pushDataSize <= maxPushDataSize - pushDataOffset, + "Descriptor heap push data exceeds maxPushDataSize"); + + static_assert((pushDataSize % 4) == 0, "push data size must be a multiple of 4"); + static_assert((pushDataOffset % 4) == 0, "push data offset must be a multiple of 4"); + } + + const vk::PushDataInfoEXT pushDataInfo{ + .offset = pushDataOffset, + // CPU memory used to read the data we will send. + .data = + vk::HostAddressRangeConstEXT{ + .address = &pushData, + .size = pushDataSize, + }, + }; + // Sends push data from the CPU to the GPU. + commandBuffer.pushDataEXT(pushDataInfo); + }; + + // Helper to create the index for an image. + auto createCombinedImageSamplerIndex = [](std::uint32_t imageIndex, std::uint32_t samplerIndex) { + // We are using a combined image-sampler index. + // That allows us to set separate image and sampler descriptor indices using a single 32 bit push data value. + + util::require(imageIndex <= 0xFFFFF, "Image index must fit in 20 bits"); + util::require(samplerIndex <= 0xFFF, "Sampler index must fit in 12 bits"); + return (samplerIndex << 20) | imageIndex; + }; + + std::optional boundShaderVariant; + + // Draw each scene node. + // This main loop will iterate through all objects in our scene. + // Check if state needs to be updated. + // Issue a draw call. + beginDebugLabel(commandBuffer, "Scene Draws", debugData.drawColor); + for (const SceneDraw& draw : m_scene.m_drawData) { + // Note: We keep track of the current bound shaders to avoid unnecessary binds. + // We use the object name to identify the call. + beginDebugLabel(commandBuffer, std::format("Draw {}", draw.m_debugName), debugData.drawColor); + + util::require(draw.m_meshId < m_scene.m_meshes.size(), "Scene draw references an invalid mesh"); + const GpuMesh& mesh = m_scene.m_meshes[draw.m_meshId]; + + // Bind a new shader group if the shader group changes. + // This is still a normal bind call, but without a pipeline object. + // Note: We keep track of the current bound shaders to avoid unnecessary binds. + if (!boundShaderVariant.has_value() || *boundShaderVariant != draw.m_shaderVariant) { + switch (draw.m_shaderVariant) { + case ShaderVariant::AlbedoAndNormal: + bindShaderGroup(m_shaderObjects.m_albedoAndNormal); + break; + case ShaderVariant::Albedo: + bindShaderGroup(m_shaderObjects.m_albedo); + break; + case ShaderVariant::SolidColor: + bindShaderGroup(m_shaderObjects.m_solidColor); + break; + default: + util::require(false, "Unsupported shader variant"); + break; + } + boundShaderVariant = draw.m_shaderVariant; + } + + // Push draw-specific data. + { + // Push draw-specific data. + // This allows us to easily set different textures for each draw call. + // Note: we need to send different data depending on the call. + + // The tutorial hardcodes sampler indices depending on the use of the texture. + // Linear sampler usually offers better quality for albedo textures. + // Nearest sampler is used for normal maps in the tutorial, for illustration purposes. + constexpr uint32_t linearSamplerIndex = 0; + constexpr uint32_t nearestSamplerIndex = 1; + + // Albedo + normal-map draws push camera + albedo and normal texture indices. + if (draw.m_shaderVariant == ShaderVariant::AlbedoAndNormal) { + + // Check that the texture is valid. + { + util::require(draw.m_albedoTextureIndex < m_scene.m_textures.size(), + "Scene draw references an invalid albedo texture"); + util::require(draw.m_normalTextureIndex < m_scene.m_textures.size(), + "Scene draw references an invalid normal texture"); + } + + // Create and set the value of the data we will push. + const DescriptorHeapDrawPushIndicesAlbedoAndNormal pushData{ + .cameraIndex = frameIndex, + .albedoTextureIndex = + createCombinedImageSamplerIndex(draw.m_albedoTextureIndex, linearSamplerIndex), + .normalTextureIndex = + createCombinedImageSamplerIndex(draw.m_normalTextureIndex, nearestSamplerIndex), + }; + + // This is a tutorial. We know that the bound buffers and textures have the correct state. + // In a real application you might need to schedule transitions to handle intermediate resources. + // Usually this is handled by a render graph, transitioning intermediate resources. + + pushDescriptorHeapShaderData(pushData); + } + // Albedo-only draws push camera + albedo texture index. + else if (draw.m_shaderVariant == ShaderVariant::Albedo) { + + util::require(draw.m_albedoTextureIndex < m_scene.m_textures.size(), + "Scene draw references an invalid albedo texture"); + + const DescriptorHeapDrawPushIndicesAlbedo pushData{ + .cameraIndex = frameIndex, + .albedoTextureIndex = + createCombinedImageSamplerIndex(draw.m_albedoTextureIndex, linearSamplerIndex), + }; + + pushDescriptorHeapShaderData(pushData); + } + // Solid color draws only need to push the camera index. + else { + util::require(draw.m_shaderVariant == ShaderVariant::SolidColor, "Unsupported shader variant"); + const DescriptorHeapDrawPushIndicesSolidColor pushData{ + .cameraIndex = frameIndex, + }; + pushDescriptorHeapShaderData(pushData); + } + } + + // Geometry binding still uses the normal Vulkan vertex path. + // Descriptor heaps in this sample are only for shader resources such as buffers and textures. + { + + beginDebugLabel(commandBuffer, "Bind Geometry", debugData.setupColor); + + // We have a single vertex buffer in this tutorial. + // Most applications benefit from separating position and non-position data into different vertex buffers. + const std::array vertexBuffers{*mesh.m_vertices.m_buffer}; + const std::array vertexOffsets{vk::DeviceSize{0}}; + + const std::array vertexSizes{mesh.m_vertices.m_size}; + const std::array vertexStrides{vk::DeviceSize{sizeof(Vertex)}}; + + constexpr uint32_t firstBinding = 0; + + // Small validation. Ensure the vertex and index buffer are valid. + { + util::require(vertexSizes.size() == vertexOffsets.size() && + vertexSizes.size() == vertexBuffers.size() && + vertexSizes.size() == vertexStrides.size(), + "Binding buffer information must have the same size"); + + util::require(mesh.m_indexCount > 0, + std::format("Draw {} has a mesh without any indices", draw.m_debugName)); + + for (uint32_t a = 0; a < vertexSizes.size(); ++a) { + util::require(vertexSizes[a] > 0, + std::format("Draw {} has a mesh with an empty vertex buffer", draw.m_debugName)); + util::require( + (vertexSizes[a] % vertexStrides[a]) == 0, + std::format("Draw {} has a mesh with an unaligned vertex buffer size", draw.m_debugName)); + } + } + + commandBuffer.bindVertexBuffers2(firstBinding, vertexBuffers, vertexOffsets, vertexSizes, vertexStrides); + + // Bind the index buffer with an explicit byte range. + constexpr vk::DeviceSize indexBufferOffset = 0; + commandBuffer.bindIndexBuffer2(*mesh.m_indices.m_buffer, indexBufferOffset, mesh.m_indices.m_size, + vk::IndexType::eUint32); + + // End Bind Geometry. + endDebugLabel(commandBuffer); + } + + // Issue the draw call to render the object. + { + // This draw call renders the current mesh and inputs and writes the output image + // for this mesh to the color attachment. + util::require(mesh.m_indexCount > 0, "Scene draw references an empty mesh"); + + const std::uint32_t firstIndex = 0; + const std::int32_t vertexOffset = 0; + + // We draw a single instance of each object, but the object index can be read in the shader as + // SV_StartInstanceLocation. + const std::uint32_t instanceCount = 1; + const std::uint32_t firstInstance = draw.m_objectIndex; + + commandBuffer.drawIndexed(mesh.m_indexCount, instanceCount, firstIndex, vertexOffset, firstInstance); + } + + // End Draw {draw.m_debugName} + endDebugLabel(commandBuffer); + } + + // End Scene Draws. + endDebugLabel(commandBuffer); + + // End dynamic rendering. + commandBuffer.endRendering(); + + // Transition the swapchain image back for presentation. + { + beginDebugLabel(commandBuffer, "Transition Swapchain To Present", debugData.barrierColor); + const ImageState presentState{ + .m_layout = vk::ImageLayout::ePresentSrcKHR, + // There is no later shader/attachment stage for presentation. The render-finished semaphore + // submitted below is what orders rendering before the presentation engine reads the image. + .m_stageMask = vk::PipelineStageFlagBits2::eNone, + .m_accessMask = vk::AccessFlagBits2::eNone, + .m_aspectMask = vk::ImageAspectFlagBits::eColor, + }; + transitionImage(commandBuffer, swapchainImage.m_image, swapchainImage.m_state, presentState, + CreateImageSubresourceRange()); + // End Transition Swapchain To Present. + endDebugLabel(commandBuffer); + } + + // End Rendering Frame {frameIndex} SwapchainImage {swapchainImageIndex} + endDebugLabel(commandBuffer); +} + +void Application::finishAndSubmitMainCommandBuffer(FrameInFlightResources& frame, std::uint32_t swapchainImageIndex) +{ + // Main synchronization objects: + // - frame.m_imageAvailableSemaphore: + // This semaphore is signaled when we acquire a new swapchain image, + // indicating that the current frame can use the swapchain image to start rendering to it. + // The current frame waits on this semaphore before writing to the swapchain, so we ensure + // swapchainAcquireWaitStage=eColorAttachmentOutput happens after the acquire. + // The semaphore is signaled when the swapchain image is acquired. + // - swapchainImage.m_renderFinishedSemaphore: + // This semaphore is signaled when the application finishes rendering. + // It indicates that the graphics queue finished work and the present queue can present the swapchain image. + // On the current frame, we signal this when the graphics queue work completes and the output is rendered. + // On the current frame, the present queue waits on this until the graphics work finishes + // and the swapchain image has been written. + // It is a per-swapchain-image resource and not a per-frame resource. + // If we reacquire the image and wait for the acquire semaphore, we know that the previous presentation of this + // swapchain image is done. The graphics queue waits on frame.m_imageAvailableSemaphore, and the present queue + // waits on the graphics queue, so we know that the acquire semaphore has been waited on before presenting. + // This allows us to safely reuse m_renderFinishedSemaphore, as Vulkan indicates that it is safe to reuse + // the present wait semaphore after the corresponding swapchain image has been acquired again and we have + // waited on the acquire semaphore or fence after that. + // - frame.m_inFlightFence: + // Indicates that the current frame's graphics queue submission is done and that we finished rendering to the + // image. This indicates we can reuse per-frame resources like command buffers. In a more complex application + // this will also allow us to reuse per-frame intermediate buffers or images. The current frame signals this + // fence when the graphics queue finishes all commands. Before acquiring the swapchain image, we can wait for + // this fence to protect per-frame resources like the command buffer. We need to ensure the previous frame + // using the same frame-in-flight slot completed before recording new commands for this frame. + // + // Per-frame (frameIndex): command buffer, intermediate buffers and images (camera buffer, depth images), + // image-available semaphore, in-flight fence. + // Per-swapchain image (swapchainImageIndex): swapchain image, render-finished semaphore. + // + // Note: It is important that we store the render-finished semaphore per swapchain and not per frame. + // The semaphore can be reused after acquiring the swapchain image, so it depends on the swapchain. + // https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html + // + // Main synchronization operations: + // - CPU waits for frame.m_inFlightFence: Previous graphics submission finished so we can use its resources. + // Per-frame resources like command buffer, camera buffer, and depth images can be reused. + // - Acquire swapchain image. + // - Signal frame.m_imageAvailableSemaphore when the swapchain is acquired. + // - Submit rendering work to the graphics queue. + // - GPU waits for frame.m_imageAvailableSemaphore. + // The GPU waits for the swapchain image to be available before executing + // swapchainAcquireWaitStage=eColorAttachmentOutput before writing to the swapchain image. + // - Signal swapchainImage.m_renderFinishedSemaphore when all commands are completed. + // - Signal frame.m_inFlightFence when all commands are completed. + // - Submit present operation to the present queue: + // - GPU waits for swapchainImage.m_renderFinishedSemaphore before presentation. + // This indicates rendering is complete before presenting the swapchain. + // + // Main timeline: + // - CPU: Wait for frame fence. + // - CPU: Acquire swapchain image. + // - CPU: Record rendering command buffer. + // - CPU: Submit rendering command buffer to the graphics queue. + // - CPU: Submit present operation to the present queue. + // - GPU graphics queue: Start executing commands in rendering command buffer. + // - GPU graphics queue: Before reaching color attachment output, + // wait for semaphore to know swapchain is available. + // - GPU graphics queue: Complete all rendering commands. + // - GPU graphics queue: Signal semaphore and fence that rendering is complete. + // - Present queue / presentation engine: Wait for the semaphore that indicates rendering is complete. + // - Present queue / presentation engine: Present the swapchain image. + // + // Note: KHR_swapchain_maintenance1 can offer more granular synchronization operations. + util::require(swapchainImageIndex < m_swapchainImages.size(), "Missing swapchain image"); + SwapchainImageResources& swapchainImage = m_swapchainImages[swapchainImageIndex]; + + // We end the command buffer before submitting it. This indicates all commands have been recorded. + frame.m_commandBuffer.end(); + + // Submit rendering operations to the graphics queue. + { + const std::array commandBufferInfos = {vk::CommandBufferSubmitInfo{ + .commandBuffer = frame.m_commandBuffer, + }}; + + // Wait until the acquired swapchain image is available before color attachment output. + // The queue submission will use this list to wait for certain operations. + // Wait semaphores are used by the queue to wait for semaphore signal operations. + // In our case, commands in swapchainAcquireWaitStage=eColorAttachmentOutput will not execute + // until imageAvailableSemaphore is signaled. + // Notice that this only affects stages in the stageMask, and will not block other stages. + const std::array waitInfos = {vk::SemaphoreSubmitInfo{ + .semaphore = *frame.m_imageAvailableSemaphore, + .stageMask = swapchainAcquireWaitStage, + }}; + + // Signal after all recorded work. + // In our case the submission signals m_renderFinishedSemaphore once all commands are done. + // This includes transitioning the image to the present layout. + // Present waits on this semaphore before the presentation engine reads the image. + const std::array signalInfos = {vk::SemaphoreSubmitInfo{ + .semaphore = *swapchainImage.m_renderFinishedSemaphore, + .stageMask = vk::PipelineStageFlagBits2::eAllCommands, + }}; + + const vk::SubmitInfo2 submitInfo{ + .waitSemaphoreInfoCount = util::safeCastToU32(waitInfos.size()), + .pWaitSemaphoreInfos = waitInfos.data(), + .commandBufferInfoCount = util::safeCastToU32(commandBufferInfos.size()), + .pCommandBufferInfos = commandBufferInfos.data(), + .signalSemaphoreInfoCount = util::safeCastToU32(signalInfos.size()), + .pSignalSemaphoreInfos = signalInfos.data(), + }; + + // Reset frame fence before submission. + { + // Reset the fence before submit so this submission can signal it. + const std::array frameFences{*frame.m_inFlightFence}; + m_logicalDevice.resetFences(frameFences); + } + + // The frame fence is signaled when the GPU finishes this submitted work. + m_graphicsQueue.submit2(submitInfo, *frame.m_inFlightFence); + } + + // Submit present operation to a present queue. + { + // Note: When creating the device, we check that the graphics and present queues have the same queue family. + // This ensures that we do not need to create barriers to transfer family ownership. + + // Present waits on the per-image render-finished semaphore, then displays the image. + const std::array waitSemaphores{*swapchainImage.m_renderFinishedSemaphore}; + + const std::array swapchains{vk::SwapchainKHR{m_vkbData.m_swapchain.swapchain}}; + const std::array imageIndices{swapchainImageIndex}; + + const vk::PresentInfoKHR presentInfo{ + .waitSemaphoreCount = util::safeCastToU32(waitSemaphores.size()), + .pWaitSemaphores = waitSemaphores.data(), + .swapchainCount = util::safeCastToU32(swapchains.size()), + .pSwapchains = swapchains.data(), + .pImageIndices = imageIndices.data(), + }; + const vk::Result presentResult = m_presentQueue.presentKHR(presentInfo); + util::require(presentResult == vk::Result::eSuccess || presentResult == vk::Result::eSuboptimalKHR, + "Failed to present swapchain image"); + } +} + +//**************** +// Helper command buffer utils +//**************** + +vk::CommandBuffer Application::getHelperCommandBuffer() const +{ + // Our application has a helper command buffer. + // This command buffer is only used during initialization to move resources on the GPU. + // + // This extra command buffer is only for setup-time uploads and layout transitions. + // Keeping it separate from frame command buffers makes initialization easier to explain: + // record all one-time copy work, submit once, wait once, then start rendering later. + + // Wrapper that gets the helper command buffer and checks that it is valid. + // The objective is to check that the command buffer exists and that it is active (we called beginHelperCommands). + + // Check that the helper command buffer exists. + util::require(m_helperCommandBuffer.m_commandBuffer, "Helper command buffer has not been allocated"); + + // Check that the command buffer is active and ready to record commands. + // Note: This is an application guard. + util::require(m_helperCommandBuffer.m_active, "Helper command buffer must be active before recording uploads"); + + // Return the command buffer so that it can be used by the application. + return m_helperCommandBuffer.m_commandBuffer; +} + +void Application::beginHelperCommands() +{ + // Small wrapper to activate the helper command buffer. + // This function resets and begins the helper command buffer, making it ready to start recording. + + // Check that the command buffer exists. + util::require(m_helperCommandBuffer.m_commandBuffer, "Helper command buffer has not been allocated"); + + // We are activating the command buffer. We need to check that it is not already active. + util::require(!m_helperCommandBuffer.m_active, "Helper command buffer is already active"); + + m_helperCommandBuffer.m_commandBuffer.reset(); + m_helperCommandBuffer.m_commandBuffer.begin( + vk::CommandBufferBeginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + // Set a debug label to easily identify this command buffer. + beginDebugLabel(m_helperCommandBuffer.m_commandBuffer, "Setup Uploads", debugData.setupColor); + + // Mark active so upload helpers can verify that setup commands are being recorded. + m_helperCommandBuffer.m_active = true; +} + +void Application::endHelperCommandsAndFlushUploads() +{ + // Submit the batched initialization work and wait for it to finish before we free staging buffers. + // This is intentionally conservative and beginner-friendly. + // + // A larger renderer would usually overlap upload work with other GPU work instead of calling waitIdle() here. + const vk::CommandBuffer commandBuffer = getHelperCommandBuffer(); + + // End recording the helper command buffer. + + // End Setup Uploads. + endDebugLabel(commandBuffer); + + // Call end to stop recording commands in the command buffer. + commandBuffer.end(); + + // Submit the helper command buffer and wait for completion. + { + const std::array commandBufferInfo = {vk::CommandBufferSubmitInfo{.commandBuffer = commandBuffer}}; + const vk::SubmitInfo2 submitInfo{ + .commandBufferInfoCount = util::safeCastToU32(commandBufferInfo.size()), + .pCommandBufferInfos = commandBufferInfo.data(), + }; + + // Use a null handle for our fence. waitIdle handles command buffer completion. + vk::Fence nullFence{}; + + m_graphicsQueue.submit2(submitInfo, nullFence); + + // Calling wait idle is not very efficient. It will probably cause CPU idle time. + // When creating the helper command buffer we could create a helper fence, and use it here to wait, + // but waiting immediately after submission is not recommended, + // and will also cause CPU idle time. + // This is not very relevant since it is only called during initialization, and will not cause stutters. + // A proper application should defer waiting to schedule CPU work during GPU initialization. + m_graphicsQueue.waitIdle(); + } + + // Reset helper command buffer state. + { + m_helperCommandBuffer.m_commandBuffer.reset(); + // Mark helper command buffer as inactive. + m_helperCommandBuffer.m_active = false; + } + + // Staging buffers were being kept alive until completion and can now be deleted. + { + // Staging buffers are only needed until their data has been copied to the correct GPU buffers. + // After the data has been copied, they can be deleted to free resources. + // We need to keep track of these buffers to delete them after the submitted commands complete. + // Note: this has a very simple strategy for teaching and learning purposes. + for (GpuBuffer& stagingBuffer : m_pendingUploadStagingBuffers) { + stagingBuffer.destroy(m_logicalDevice); + } + m_pendingUploadStagingBuffers.clear(); + } +} + +//**************** +// Destruction and cleanup +//**************** + +Application::~Application() +{ + // When destroying the application, call cleanup to destroy Vulkan and other resources. + cleanup(); +} + +void Application::cleanup() +{ + // This function is called when the application terminates to destroy all resources. + // Note: we are assuming no errors during destruction. + + // Wait before destroying resources that the GPU may still be using. + if (m_logicalDevice) { + // We are using a simple path in this tutorial. + // wait idle is the easiest way to ensure there is no outstanding work on the GPU queues. + // We are going to destroy buffers and images so we need to ensure that there is no work on the GPU using them. + // + // Note: consider using KHR_swapchain_maintenance1 for better swapchain resource management. + m_logicalDevice.waitIdle(); + } + // Destroy Vulkan resources. + { + // Vulkan is a low-level API so we need to manually clean up our resources. + // Vulkan-Hpp greatly helps manage resource destruction as it offers RAII wrappers. + // Most of our Vulkan objects are owned by vk::Unique* Vulkan-Hpp wrappers like vk::UniqueSemaphore. + // These wrappers will call Vulkan destroy functions like vkDestroySemaphore in their destructor. + // Notice that we need to destroy and free our Vulkan objects in dependency order. + // For example, we cannot destroy a device until we destroy all objects like buffers created by the device. + // Vulkan-Hpp also offers vk::raii wrappers as a higher-level RAII wrapper. + + m_shaderObjects.m_solidColor.m_fragment.reset(); + m_shaderObjects.m_solidColor.m_vertex.reset(); + m_shaderObjects.m_albedo.m_fragment.reset(); + m_shaderObjects.m_albedo.m_vertex.reset(); + m_shaderObjects.m_albedoAndNormal.m_fragment.reset(); + m_shaderObjects.m_albedoAndNormal.m_vertex.reset(); + + m_descriptorHeaps.m_samplerHeap.m_buffer.destroy(m_logicalDevice); + m_descriptorHeaps.m_resourceHeap.m_buffer.destroy(m_logicalDevice); + m_descriptorHeaps = {}; + + for (GpuImage& image : m_scene.m_textures) { + image.destroy(); + } + m_scene.m_textures.clear(); + + for (GpuMesh& mesh : m_scene.m_meshes) { + mesh.m_indices.destroy(m_logicalDevice); + mesh.m_vertices.destroy(m_logicalDevice); + } + m_scene.m_meshes.clear(); + m_scene.m_objects.destroy(m_logicalDevice); + m_scene.m_pointLight.destroy(m_logicalDevice); + m_scene.m_solidColor.destroy(m_logicalDevice); + m_scene = {}; + + for (GpuBuffer& stagingBuffer : m_pendingUploadStagingBuffers) { + stagingBuffer.destroy(m_logicalDevice); + } + m_pendingUploadStagingBuffers.clear(); + + // Command buffers are owned by the command pool, so clear the non-owning handles first. + m_helperCommandBuffer = {}; + + for (FrameInFlightResources& frame : m_framesInFlight) { + frame.m_camera.destroy(m_logicalDevice); + frame.m_commandBuffer = nullptr; + frame.m_inFlightFence.reset(); + frame.m_imageAvailableSemaphore.reset(); + + frame.m_depthImage.destroy(); + } + + // Destroying the command pool frees all command buffers allocated from it. + m_commandPool.reset(); + + // Clearing the vector destroys image views and per-image semaphores. + m_swapchainImages.clear(); + } + + // Destroy Vulkan bootstrap resources. + { + if (m_vkbData.m_swapchain.swapchain != VK_NULL_HANDLE) { + // Note: Swapchain images are owned by the swapchain. We destroy app images, but not the swapchain images. + // To destroy swapchain images we have to destroy the swapchain. + vkb::destroy_swapchain(m_vkbData.m_swapchain); + m_vkbData.m_swapchain = {}; + } + + // Destroy in dependency order, which is usually reverse creation order: + // We need to destroy Vulkan objects in dependency order. + // We cannot destroy an instance if the device still exists. + if (m_vkbData.m_device.device != VK_NULL_HANDLE) { + vkb::destroy_device(m_vkbData.m_device); + m_vkbData.m_device = {}; + m_logicalDevice = nullptr; + } + + if (m_surface) { + vkb::destroy_surface(m_vkbData.m_instance, static_cast(m_surface)); + m_surface = nullptr; + } + + if (m_vkbData.m_instance.instance != VK_NULL_HANDLE) { + vkb::destroy_instance(m_vkbData.m_instance); + m_vkbData.m_instance = {}; + } + } + + // Destroy and terminate GLFW. + { + if (m_window != nullptr) { + glfwDestroyWindow(m_window); + m_window = nullptr; + } + glfwTerminate(); + } +} + +//**************** +// Resource utils +//**************** + +std::uint32_t Application::findMemoryType(std::uint32_t typeBits, vk::MemoryPropertyFlags properties) const +{ + // Find a valid memory type index to allocate memory for a buffer or image. + // m_memoryTypeFlags was cached from vk::PhysicalDeviceMemoryProperties during device initialization. + // + // Iterate through the cached memory flags and select the first compatible one. + for (std::uint32_t i = 0; i < m_memoryTypeFlags.size(); ++i) { + const bool typeMatches = (typeBits & (1U << i)) != 0U; + const bool flagsMatch = (m_memoryTypeFlags[i] & properties) == properties; + if (typeMatches && flagsMatch) { + return i; + } + } + + throw std::runtime_error("No compatible Vulkan memory type found"); +} + +Application::GpuBuffer Application::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags memoryProperties, + std::string_view debugName) const +{ + // This function creates a buffer. + // Buffers in Vulkan store linear data. They are commonly used to store scalars, arrays, or matrices. + + util::require(size > 0, "Vulkan requires buffer size to be greater than 0."); + util::require(usage != vk::BufferUsageFlags{}, "Trying to create a buffer without any usage flags."); + + GpuBuffer buffer{.m_size = size}; + + // Create the buffer object. + { + const vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive, + }; + buffer.m_buffer = m_logicalDevice.createBufferUnique(bufferInfo); + } + + // Name resources like buffers to help when debugging in tools like RenderDoc. + setDebugName(*buffer.m_buffer, vk::ObjectType::eBuffer, std::string{debugName}); + + // Only buffers created with eShaderDeviceAddress receive a GPU address. That keeps staging, + // vertex, and index buffers simple while still supporting descriptor heap buffer descriptors. + // Note: we cache the GPU address of all buffers that have one. + const bool needsGpuAddress = (usage & vk::BufferUsageFlagBits::eShaderDeviceAddress) != vk::BufferUsageFlags{}; + + // Allocate memory and bind it to the buffer. + { + // Use the created buffer object to query the memory requirements. + const vk::BufferMemoryRequirementsInfo2 requirementsInfo{ + .buffer = *buffer.m_buffer, + }; + const vk::MemoryRequirements2 requirements = m_logicalDevice.getBufferMemoryRequirements2(requirementsInfo); + + // Set allocation flags, enabling support for device addresses if needed. + const vk::MemoryAllocateFlagsInfo allocateFlags{ + .flags = needsGpuAddress ? vk::MemoryAllocateFlagBits::eDeviceAddress : vk::MemoryAllocateFlags{}, + }; + + // Use the queried requirements to allocate the buffer. + const vk::MemoryAllocateInfo allocateInfo{ + .pNext = (allocateFlags.flags != vk::MemoryAllocateFlags{}) ? &allocateFlags : nullptr, + .allocationSize = requirements.memoryRequirements.size, + .memoryTypeIndex = findMemoryType(requirements.memoryRequirements.memoryTypeBits, memoryProperties), + }; + + // We allocate one memory block per buffer for simplicity. + // Real applications suballocate from larger memory blocks to reduce overhead and fragmentation. + // Consider using a library for memory management such as VMA instead. + buffer.m_memory = m_logicalDevice.allocateMemoryUnique(allocateInfo); + + // We bind the memory to the buffer. + // We created the buffer and allocated memory for the buffer. + // This binds and links this memory to the buffer. + const std::array bindInfos{vk::BindBufferMemoryInfo{ + .buffer = *buffer.m_buffer, + .memory = *buffer.m_memory, + .memoryOffset = 0, + }}; + m_logicalDevice.bindBufferMemory2(bindInfos); + } + + // Retrieve the GPU address. + if (needsGpuAddress) { + const vk::BufferDeviceAddressInfo addressInfo{.buffer = *buffer.m_buffer}; + buffer.m_addressGPU = m_logicalDevice.getBufferAddress(addressInfo); + util::require(buffer.m_addressGPU != 0, "Failed to get buffer GPU address"); + } + + // Host-visible memory can be accessed by the CPU. + // We keep those buffers persistently mapped so setup code can upload data with a simple memcpy. + // This is a simple tutorial, so we avoid flush and invalidate operations by using eHostCoherent. + const bool hostVisible = (memoryProperties & vk::MemoryPropertyFlagBits::eHostVisible) != vk::MemoryPropertyFlags{}; + + if (hostVisible) { + // We map the buffer and always keep it mapped. + buffer.m_addressCPU = m_logicalDevice.mapMemory(*buffer.m_memory, 0, size); + + util::require(buffer.m_addressCPU != nullptr, "Failed to map buffer CPU address"); + + util::require((memoryProperties & vk::MemoryPropertyFlagBits::eHostCoherent) == + vk::MemoryPropertyFlagBits::eHostCoherent, + "Mapped buffers in this tutorial must be host coherent to avoid manual flushes"); + } + + return buffer; +} + +Application::GpuBuffer Application::uploadToNewStagingBuffer(std::span data, + std::string_view debugName) const +{ + // Staging buffers are temporary buffers used to transfer data from the CPU to the GPU. + // They let us use a CPU-visible buffer before uploading data to a GPU-only buffer. + + // This is a tutorial, so they are created on demand for each upload. + // Real applications will reuse them and suballocate from them to reduce overhead. + + // Staging buffers are host coherent to make CPU-to-GPU uploads simple. + const vk::MemoryPropertyFlags stagingMemoryProperties = + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent; + + GpuBuffer stagingBuffer = createBuffer(static_cast(data.size()), + vk::BufferUsageFlagBits::eTransferSrc, stagingMemoryProperties, debugName); + + uploadBuffer(stagingBuffer, data); + return stagingBuffer; +} + +Application::GpuBuffer Application::uploadToNewGpuBuffer(std::span data, + vk::BufferUsageFlags finalUsage, std::string_view debugName) +{ + // This function creates a GPU buffer and initializes it by copying data into it. + + // We use the helper command buffer to record copies to the buffer. + const vk::CommandBuffer commandBuffer = getHelperCommandBuffer(); + + // Static GPU data is uploaded through a temporary host-visible staging buffer, + // then kept in device-local memory for rendering. + + // Allocate the staging buffer and upload data to it. + const std::string stagingName = std::format("{} Staging", debugName); + GpuBuffer stagingBuffer = uploadToNewStagingBuffer(data, stagingName); + + // Allocate GPU-only buffer. + GpuBuffer gpuBuffer = createBuffer(static_cast(data.size()), + // We add eTransferDst usage to allow copying data to the buffer. + finalUsage | vk::BufferUsageFlagBits::eTransferDst, + // Mark it device local: GPU-only buffers are usually faster to access. + vk::MemoryPropertyFlagBits::eDeviceLocal, debugName); + + // Add a debug label to help identify how the buffer is set up. + const std::string uploadDebugLabel = std::format("Upload Buffer: {}", debugName); + beginDebugLabel(commandBuffer, uploadDebugLabel, debugData.transferColor); + + // Copy staging buffer data to the GPU buffer using the active helper command buffer. + { + const std::array copyRegion = {vk::BufferCopy2{ + // Offsets are 0 since we copy the entire buffer. + .srcOffset = 0, + .dstOffset = 0, + .size = static_cast(data.size()), + }}; + const vk::CopyBufferInfo2 copyInfo{ + .srcBuffer = *stagingBuffer.m_buffer, + .dstBuffer = *gpuBuffer.m_buffer, + .regionCount = util::safeCastToU32(copyRegion.size()), + .pRegions = copyRegion.data(), + }; + commandBuffer.copyBuffer2(copyInfo); + } + + // Buffer memory barrier to ensure GPU buffer data is available. + { + const std::array uploadBarrier = {vk::BufferMemoryBarrier2{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .dstAccessMask = vk::AccessFlagBits2::eMemoryRead, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = *gpuBuffer.m_buffer, + .offset = 0, + .size = static_cast(data.size()), + }}; + const vk::DependencyInfo dependencyInfo{ + .bufferMemoryBarrierCount = util::safeCastToU32(uploadBarrier.size()), + .pBufferMemoryBarriers = uploadBarrier.data(), + }; + commandBuffer.pipelineBarrier2(dependencyInfo); + } + // End Upload Buffer. + endDebugLabel(commandBuffer); + + // The GPU buffer is stored by the application, but staging buffers need to be alive until + // the submitted GPU work that references them is done. + // The tutorial keeps track of all staging buffers. + // They will be freed once the helper command buffer finishes execution. + m_pendingUploadStagingBuffers.push_back(std::move(stagingBuffer)); + return gpuBuffer; +} + +void Application::uploadBuffer(const GpuBuffer& buffer, std::span data) const +{ + // Mapped buffers uploaded by this tutorial use coherent host-visible memory, so memcpy is enough. + // This allows us to make things simple and avoid invalidation and flushing. + // + // With coherent memory we still need to ensure the CPU does not overwrite data while the GPU reads it. + // Staging buffers are written before submission. + // Per-frame buffers are protected by a fence before submission. + + util::require(buffer.m_addressCPU != nullptr, "Buffer must be persistently mapped for CPU upload"); + util::require(data.size() <= buffer.m_size, "Upload is larger than destination buffer"); + + std::memcpy(buffer.m_addressCPU, data.data(), data.size()); +} + +void Application::allocateGpuImage(GpuImage& image) const +{ + // Consider using a library like VMA to allocate and suballocate memory for images and buffers. + util::require(static_cast(image.m_image), "GpuImage must exist before allocating memory"); + + const vk::ImageMemoryRequirementsInfo2 requirementsInfo{ + .image = *image.m_image, + }; + const vk::MemoryRequirements2 requirements = m_logicalDevice.getImageMemoryRequirements2(requirementsInfo); + const vk::MemoryAllocateInfo allocateInfo{ + .allocationSize = requirements.memoryRequirements.size, + .memoryTypeIndex = + findMemoryType(requirements.memoryRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal), + }; + image.m_memory = m_logicalDevice.allocateMemoryUnique(allocateInfo); + + const std::array bindInfos{vk::BindImageMemoryInfo{ + .image = *image.m_image, + .memory = *image.m_memory, + .memoryOffset = 0, + }}; + m_logicalDevice.bindImageMemory2(bindInfos); +} + +Application::GpuImage Application::createTexture(const util::ImageRgba8& sourceImage, std::string_view debugName) +{ + // This function creates a texture. + // This creates a read-only texture image. + + util::require(sourceImage.m_width > 0 && sourceImage.m_height > 0, + "Texture images must have non-zero width and height"); + util::require(static_cast(sourceImage.m_height) * static_cast(sourceImage.m_width) == + sourceImage.m_pixels.size(), + "Invalid image size."); + + // We create a GPU image to store our texture. + GpuImage image; + image.m_mipLevels = static_cast( + std::floor(std::log2(static_cast(std::max(sourceImage.m_width, sourceImage.m_height)))) + 1.0F); + image.m_states.resize(image.m_mipLevels, ImageState{.m_aspectMask = vk::ImageAspectFlagBits::eColor}); + + // Create the GPU image object. + { + const vk::ImageCreateInfo imageInfo{ + .imageType = vk::ImageType::e2D, + .format = mainTextureFormat, + .extent = vk::Extent3D{.width = sourceImage.m_width, + .height = sourceImage.m_height, + // Vulkan supports 3D images, but this tutorial only uses 2D, so depth is 1. + .depth = 1}, + // Images are stored using mipmap levels with prefiltered versions of the image at smaller resolutions. + // This improves memory access and allows texture interpolation for better image quality. + .mipLevels = image.m_mipLevels, + // Use more than 1 for texture arrays and cube maps. + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + // Avoid linear tiling. + // Optimal tiling makes memory layout opaque but it enables proper access for sampling. + .tiling = vk::ImageTiling::eOptimal, + // Selecting the correct usage flags can have performance implications. + // Only select the ones you need. For example, avoid eStorage if the image is not GPU written. + .usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | + vk::ImageUsageFlagBits::eSampled, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + image.m_image = m_logicalDevice.createImageUnique(imageInfo); + } + setDebugName(*image.m_image, vk::ObjectType::eImage, std::string{debugName}); + + // Allocate memory and bind it to the image. + allocateGpuImage(image); + + // Upload the image from CPU memory to GPU memory, and generate mip levels. + { + // Create a temporary staging buffer that is visible to the CPU and copy the image there. + // This tutorial creates a new staging buffer for each image so the upload path stays explicit. + const std::string stagingName = std::format("{} Staging", debugName); + GpuBuffer stagingBuffer = uploadToNewStagingBuffer(std::as_bytes(std::span{sourceImage.m_pixels}), stagingName); + + // Record setup-time copy work into the active helper command buffer. + const vk::CommandBuffer commandBuffer = getHelperCommandBuffer(); + + // Add a debug label to identify this work. + const std::string uploadDebugLabel = + debugName.empty() ? std::string{"Upload Texture"} : std::format("Upload Texture: {}", debugName); + beginDebugLabel(commandBuffer, uploadDebugLabel, debugData.transferColor); + + const ImageState transferDstState{ + .m_layout = vk::ImageLayout::eTransferDstOptimal, + .m_stageMask = vk::PipelineStageFlagBits2::eTransfer, + .m_accessMask = vk::AccessFlagBits2::eTransferWrite, + .m_aspectMask = vk::ImageAspectFlagBits::eColor, + }; + const ImageState transferSrcState{ + .m_layout = vk::ImageLayout::eTransferSrcOptimal, + .m_stageMask = vk::PipelineStageFlagBits2::eTransfer, + .m_accessMask = vk::AccessFlagBits2::eTransferRead, + .m_aspectMask = vk::ImageAspectFlagBits::eColor, + }; + const ImageState shaderReadState{ + .m_layout = vk::ImageLayout::eShaderReadOnlyOptimal, + .m_stageMask = vk::PipelineStageFlagBits2::eFragmentShader, + .m_accessMask = vk::AccessFlagBits2::eShaderSampledRead, + .m_aspectMask = vk::ImageAspectFlagBits::eColor, + }; + + // Mip generation still starts with one whole-image transition. + // Mip 0 can receive the upload. + // Higher mips can start as blit destinations. + image.transition(commandBuffer, transferDstState); + + // Copy the staging buffer into the image at mip 0. + { + const std::array copyRegion = {vk::BufferImageCopy2{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = + vk::ImageSubresourceLayers{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .imageOffset = vk::Offset3D{.x = 0, .y = 0, .z = 0}, + .imageExtent = vk::Extent3D{.width = sourceImage.m_width, .height = sourceImage.m_height, .depth = 1}, + }}; + const vk::CopyBufferToImageInfo2 copyInfo{ + .srcBuffer = *stagingBuffer.m_buffer, + .dstImage = *image.m_image, + .dstImageLayout = transferDstState.m_layout, + .regionCount = util::safeCastToU32(copyRegion.size()), + .pRegions = copyRegion.data(), + }; + commandBuffer.copyBufferToImage2(copyInfo); + } + + // Use blit operations to generate all remaining mip levels for this image. + // + // The idea is to copy the previous mip level to the current mip level, + // using a blit operation to resize the image. + // + // Mip levels let sampling choose a lower-resolution version of a texture when appropriate, + // improving performance and reducing shimmering. This improves the application's visual quality. + // We generate the mip levels by blitting down the image. + { + std::int32_t sourceWidth = util::safeCastTo(sourceImage.m_width); + std::int32_t sourceHeight = util::safeCastTo(sourceImage.m_height); + + // Iterate through all mip levels. + for (std::uint32_t mipLevel = 1; mipLevel < image.m_mipLevels; ++mipLevel) { + // Transition the previous mip level from transfer dst to transfer src. + // Note: blit is a transfer operation. + image.transition(commandBuffer, transferSrcState, mipLevel - 1, 1); + + // Half the resolution of the previous mip level. + const std::int32_t destinationWidth = std::max(1, sourceWidth / 2); + const std::int32_t destinationHeight = std::max(1, sourceHeight / 2); + + // Issue a blit command to generate this level from the previous level. + // A blit is a copy-and-resize operation. + // Note: During device creation we ensure support for linear blit operations on this format. + { + const vk::ImageBlit2 blitRegion{ + .srcSubresource = + vk::ImageSubresourceLayers{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = mipLevel - 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .srcOffsets = std::array{vk::Offset3D{.x = 0, .y = 0, .z = 0}, + vk::Offset3D{.x = sourceWidth, .y = sourceHeight, .z = 1}}, + .dstSubresource = + vk::ImageSubresourceLayers{ + .aspectMask = vk::ImageAspectFlagBits::eColor, + .mipLevel = mipLevel, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .dstOffsets = std::array{vk::Offset3D{.x = 0, .y = 0, .z = 0}, + vk::Offset3D{.x = destinationWidth, .y = destinationHeight, .z = 1}}, + }; + // These layout fields do not transition the image. They must match the layouts produced by the + // surrounding barriers, so we keep them sourced from the same states used for image transition. + const vk::BlitImageInfo2 blitInfo{ + .srcImage = *image.m_image, + .srcImageLayout = transferSrcState.m_layout, + .dstImage = *image.m_image, + .dstImageLayout = transferDstState.m_layout, + .regionCount = 1, + .pRegions = &blitRegion, + .filter = vk::Filter::eLinear, + }; + commandBuffer.blitImage2(blitInfo); + } + + // Update current level dimensions. + sourceWidth = destinationWidth; + sourceHeight = destinationHeight; + } + + image.transition(commandBuffer, shaderReadState); + } + // End Upload Texture. + endDebugLabel(commandBuffer); + + // Keep the staging buffer alive until the batched helper command buffer has finished. + m_pendingUploadStagingBuffers.push_back(std::move(stagingBuffer)); + } + return image; +} + +// Helper to set debug names on Vulkan objects. +// Debug names improve debugging in tools like RenderDoc and Nsight Graphics. +// They also make most validation-layer messages easier to interpret. +template +void Application::setDebugName(Handle handle, vk::ObjectType objectType, const std::string& name) const +{ + // Return early if GPU debugging is disabled. + if (!debugData.enableGpuDebug) { + return; + } + + util::require(static_cast(m_logicalDevice), "Debug naming requires a valid Vulkan device"); + util::require(!name.empty(), "Debug empty names are allowed by Vulkan, but not by the application"); + + using RawHandle = typename Handle::CType; + const RawHandle rawHandle = static_cast(handle); + util::require(rawHandle != VK_NULL_HANDLE, "Debug naming requires a valid Vulkan object handle"); + + const vk::DebugUtilsObjectNameInfoEXT nameInfo{ + .objectType = objectType, + .objectHandle = util::rawHandleToUint64(rawHandle), + // Vulkan expects pObjectName to be null-terminated; std::string::c_str() guarantees that. + .pObjectName = name.c_str(), + }; + static_cast(m_logicalDevice.setDebugUtilsObjectNameEXT(nameInfo)); +} + +void Application::beginDebugLabel(vk::CommandBuffer commandBuffer, const std::string& name, + const std::array& color) +{ + // Return early if GPU debugging is disabled. + if (!debugData.enableGpuDebug) { + return; + } + + const vk::DebugUtilsLabelEXT label{ + .pLabelName = name.c_str(), + .color = color, + }; + + // We assume command buffer is recording + commandBuffer.beginDebugUtilsLabelEXT(label); +} + +void Application::endDebugLabel(vk::CommandBuffer commandBuffer) +{ + // Return early if GPU debugging is disabled. + if constexpr (!debugData.enableGpuDebug) { + return; + } + + commandBuffer.endDebugUtilsLabelEXT(); +} + +// Helper function that records a barrier to transition one image range from one tracked state to another. +void Application::transitionImage(vk::CommandBuffer commandBuffer, vk::Image image, ImageState& currentState, + const ImageState& newState, vk::ImageSubresourceRange subresourceRange) +{ + // This tutorial uses a simple implementation of how to track resources for synchronization, allowing us to create + // automatic barriers. + // A proper Render Hardware Interface (RHI) would use this function to automatically synchronize resources. + // + // Our implementation is very simple. For a production ready approach check: + // - Vulkanised 2024: Vulkan Synchronization for WebGPU - https://www.youtube.com/watch?v=SH0N4QmioUw + // - Vulkanised 2024: Vulkan Synchronization Made Easy - https://www.youtube.com/watch?v=d15RXWp1Rqo + // + // Many renderers implement a render graph or task graph. + // A render graph is capable of creating barriers and other synchronization primitives automatically. + // They can reorder work and passes to increase GPU overlap and avoid idle time on the GPU. Advanced + // implementations can also reuse resources to reduce memory consumption. They are the standard solution + // in the industry, so we recommend most developers study render graphs next. + // + // ImageState is tutorial-side bookkeeping: Vulkan tracks real image layouts during execution, but the app still + // needs to remember the layout/access/stage it last requested so it can build the next correct barrier. + + // Skip only exact duplicate read-after-read barriers. + // Widening or narrowing a read scope later would require remembering the earlier write that made the image contents + // visible, which this small tutorial tracker intentionally does not keep. + { + const auto onlyRead = [](const ImageState& state) { + const vk::AccessFlags2 writeAccessMask = + vk::AccessFlagBits2::eShaderWrite | vk::AccessFlagBits2::eColorAttachmentWrite | + vk::AccessFlagBits2::eDepthStencilAttachmentWrite | vk::AccessFlagBits2::eTransferWrite | + vk::AccessFlagBits2::eHostWrite | vk::AccessFlagBits2::eMemoryWrite; + return static_cast(state.m_accessMask) && !static_cast(state.m_accessMask & writeAccessMask); + }; + + const bool duplicateReadOnlyState = currentState == newState && onlyRead(currentState); + + // Limited example of removing redundant read-only barriers. + // We can improve this with a more complex state tracker and better heuristics. + // Instead of per-resource state tracking, a lot of games implement a render-graph. + // Render graphs allow reordering commands to increase overlap and reduce synchronization overhead. + if (duplicateReadOnlyState) { + return; + } + } + + // Vulkan requires images to be in a layout that matches their current use. This barrier + // both changes the layout and describes which earlier writes must be visible to later work. + const std::array barriers = {vk::ImageMemoryBarrier2{ + .srcStageMask = currentState.m_stageMask, + .srcAccessMask = currentState.m_accessMask, + .dstStageMask = newState.m_stageMask, + .dstAccessMask = newState.m_accessMask, + .oldLayout = currentState.m_layout, + .newLayout = newState.m_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = subresourceRange, + }}; + + // Synchronization2 groups barriers in a DependencyInfo structure before recording them. + const vk::DependencyInfo dependencyInfo{ + .imageMemoryBarrierCount = util::safeCastToU32(barriers.size()), + .pImageMemoryBarriers = barriers.data(), + }; + commandBuffer.pipelineBarrier2(dependencyInfo); + + // Update the tracked state for this image range to the new state requested by this barrier. + currentState = newState; +} + +//**************** +// GpuBuffer +//**************** + +void Application::GpuBuffer::destroy(vk::Device device) +{ + // Free GPU buffer and associated resources + + if (m_addressCPU != nullptr) { + device.unmapMemory(*m_memory); + m_addressCPU = nullptr; + } + m_buffer.reset(); + m_memory.reset(); + m_addressGPU = {}; + m_size = {}; +} + +//**************** +// GpuImage +//**************** + +void Application::GpuImage::destroy() +{ + // Free GPU image and associated resources + + m_image.reset(); + m_memory.reset(); + m_mipLevels = 1; + m_states.clear(); +} + +void Application::GpuImage::transition(vk::CommandBuffer commandBuffer, const ImageState& newState) +{ + // Wrapper to transition all mip levels at once + transition(commandBuffer, newState, 0, m_mipLevels); +} + +void Application::GpuImage::transition(vk::CommandBuffer commandBuffer, const ImageState& newState, + std::uint32_t baseMipLevel, std::uint32_t mipLevelCount) +{ + // Wrapper to call transitionImage and change and synchronize an image + // This will record and transition mip levels from baseMipLevel to mipLevelCount to new state + + // ImageState is not a Vulkan handle. + // It is this tutorial's small state tracker so each transition can use the previous layout/access/stage values as + // the source of the next barrier. + + // Small validation + { + util::require(static_cast(m_image), "GpuImage must exist before transitioning it"); + util::require(!m_states.empty(), "GpuImage must track at least one image state"); + util::require(mipLevelCount > 0, "GpuImage transition must cover at least one mip level"); + util::require(baseMipLevel < m_states.size(), "GpuImage transition base mip level is out of range"); + util::require((baseMipLevel + mipLevelCount) <= m_states.size(), + "GpuImage transition range exceeds tracked mip levels"); + } + + const std::uint32_t endMipLevel = baseMipLevel + mipLevelCount; + + // Transition images from runBaseMipLevel to runBaseMipLevel+runMipLevelCount + // This is done in a single barrier. + const auto transitionRun = [&](const std::uint32_t runBaseMipLevel, const std::uint32_t runMipLevelCount, + ImageState runState) { + transitionImage(commandBuffer, *m_image, runState, newState, + CreateImageSubresourceRange(runBaseMipLevel, newState.m_aspectMask, runMipLevelCount)); + for (std::uint32_t mipLevel = runBaseMipLevel; mipLevel < runBaseMipLevel + runMipLevelCount; ++mipLevel) { + m_states[mipLevel] = runState; + } + }; + + // Start handling the first mip level + std::uint32_t runBaseMipLevel = baseMipLevel; + ImageState runState = m_states[runBaseMipLevel]; + + // The loop will store the first mip level it has to transition in runBaseMipLevel, + for (std::uint32_t mipLevel = baseMipLevel + 1; mipLevel < endMipLevel; ++mipLevel) { + // All mip levels between runBaseMipLevel and mipLevel have the same state + if (m_states[mipLevel] != runState) { + // Different state found in this mip level, so we need to create a new barrier. + + // Transition current mip levels, note that they share the same state + // Consecutive mips that already share the same source state can use one wider barrier. + transitionRun(runBaseMipLevel, mipLevel - runBaseMipLevel, runState); + + // Store current state of mip level and try to find more additional mip levels that are compatible. + runBaseMipLevel = mipLevel; + runState = m_states[mipLevel]; + } + } + + // The loop will store the first mip level pending transition in runBaseMipLevel, + // all mip levels between runBaseMipLevel and the end of the loop have the same state and can use the same + // transition barrier. This last transition covers pending mips still not transitioned by the loop, including the + // common case of all mips sharing the same state. + transitionRun(runBaseMipLevel, endMipLevel - runBaseMipLevel, runState); +} + +void Application::GpuViewImage::destroy() +{ + m_imageView.reset(); + m_image.destroy(); +} + +} // namespace siggraph + +int main(int argc, char** argv) +{ + try { + siggraph::Application app; + app.setFrameLimit(siggraph::util::readFrameLimitCLI(argc, argv)); + app.run(); + } + catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/attachments/siggraph2026_vk_tutorial/src/main.h b/attachments/siggraph2026_vk_tutorial/src/main.h new file mode 100644 index 00000000..f6f1e9a2 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/src/main.h @@ -0,0 +1,576 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "util.h" + +struct GLFWwindow; + +namespace siggraph { + +struct ShaderFilePair { + std::string_view vertexName; + std::string_view fragmentName; +}; + +// Helper with all scene paths. This makes it easy to change the scene for testing. +namespace SceneData { + +constexpr std::array sceneFileNames = {"external/sponza/Models/Sponza/glTF/Sponza.gltf", + "SponzaExtras/VulkanLogo/VulkanLogo.gltf", + "SponzaExtras/camera.gltf"}; + +// We have three shader variants: albedo with normal map, albedo only, and solid color. +constexpr ShaderFilePair albedoAndNormalShaderFilePair{.vertexName = "basic.vert.spv", + .fragmentName = "basic.frag.spv"}; + +constexpr ShaderFilePair albedoShaderFilePair{.vertexName = "basic.vert.spv", .fragmentName = "albedo.frag.spv"}; + +// The solid-color shader is only used for objects like the Vulkan logo that lack textures in the glTF. +constexpr ShaderFilePair solidColorShaderFilePair{.vertexName = "basic.vert.spv", + .fragmentName = "solid_color.frag.spv"}; + +} // namespace SceneData + +// Owns the window, Vulkan bootstrap objects, and the render loop for the tutorial. +// Keeping ownership in one class makes destruction order explicit and easy to follow. +class Application { +public: + Application() = default; + + // The destructor calls cleanup(), so destroying the Application object releases Vulkan and GLFW resources. + ~Application(); + + // Vulkan objects are unique OS/GPU handles. Copying the Application would duplicate + // ownership incorrectly, so copying is disabled. + Application(const Application&) = delete; + Application& operator=(const Application&) = delete; + + // Entry point for the tutorial: create the window, initialize Vulkan, render until close. + void run(); + + void setFrameLimit(std::uint32_t frameLimit); + +private: + // Helper to initialize image subresource ranges with common defaults. + static constexpr vk::ImageSubresourceRange + CreateImageSubresourceRange(const std::uint32_t baseMipLevel = 0, + const vk::ImageAspectFlags aspectMask = vk::ImageAspectFlagBits::eColor, + const std::uint32_t levelCount = 1) + { + return vk::ImageSubresourceRange{ + .aspectMask = aspectMask, + .baseMipLevel = baseMipLevel, + .levelCount = levelCount, + .baseArrayLayer = 0, + .layerCount = 1, + }; + } + + // Helper to keep track of the state of an image to issue barriers. + struct ImageState { + vk::ImageLayout m_layout = vk::ImageLayout::eUndefined; + vk::PipelineStageFlags2 m_stageMask = vk::PipelineStageFlagBits2::eNone; + vk::AccessFlags2 m_accessMask = vk::AccessFlagBits2::eNone; + vk::ImageAspectFlags m_aspectMask = vk::ImageAspectFlagBits::eColor; + + bool operator==(const ImageState&) const = default; + }; + + // Wrapper around a GPU buffer and its related resources. + struct GpuBuffer { + // Memory is declared before the buffer because members are destroyed in reverse order. + // That lets the Vulkan-Hpp unique buffer release the VkBuffer before the memory is freed. + vk::UniqueDeviceMemory m_memory{}; + vk::UniqueBuffer m_buffer{}; + vk::DeviceAddress m_addressGPU{}; + vk::DeviceSize m_size = 0; + void* m_addressCPU = nullptr; + + void destroy(vk::Device device); + }; + + // Wrapper around a GPU image and its related resources. + struct GpuImage { + // Image memory must outlive the image bound to it. + vk::UniqueDeviceMemory m_memory{}; + vk::UniqueImage m_image{}; + std::uint32_t m_mipLevels = 1; + std::vector m_states{}; + + void destroy(); + void transition(vk::CommandBuffer commandBuffer, const ImageState& newState); + void transition(vk::CommandBuffer commandBuffer, const ImageState& newState, std::uint32_t baseMipLevel, + std::uint32_t mipLevelCount); + }; + + struct GpuViewImage { + // Image views must be destroyed before the image. + GpuImage m_image{}; + vk::UniqueImageView m_imageView{}; + + void destroy(); + }; + + // Resources used during rendering. + // We can render multiple frames in flight, so we have to duplicate some resources. + // This allows the CPU to start preparing frame N+1 before the GPU finishes frame N. + struct FrameInFlightResources { + vk::CommandBuffer m_commandBuffer{}; + GpuBuffer m_camera{}; + + vk::UniqueSemaphore m_imageAvailableSemaphore{}; + vk::UniqueFence m_inFlightFence{}; + + GpuViewImage m_depthImage{}; + }; + + // Resources that belong to each swapchain image. + // Swapchain images are owned by Vulkan; we own views and per-image synchronization around them. + struct SwapchainImageResources { + // The image provided by the swapchain. We render directly into it. + // Owned by the swapchain, so we don't destroy it. + // This is a non-owning copy; we only keep track of it for synchronization. + vk::Image m_image{}; + + vk::UniqueImageView m_imageView{}; + + // Presentation can keep waiting on this binary semaphore. + // Keep one per swapchain image so we only reuse it after that image is acquired again, + // and we wait for the acquire semaphore. + vk::UniqueSemaphore m_renderFinishedSemaphore{}; + + // Track the swapchain image layout between frames so the next render pass knows the + // correct source state for its first barrier. + ImageState m_state{}; + }; + + struct HelperCommandBuffer { + vk::CommandBuffer m_commandBuffer{}; + bool m_active = false; + }; + + struct CameraData { + glm::mat4 viewProjection; + glm::vec4 cameraPosition; + }; + + // Object data lives in a storage buffer. The vertex shader reads one model matrix per instance. + struct alignas(16) ObjectData { + glm::mat4 model; + }; + static_assert(sizeof(ObjectData) == 64); + + struct alignas(16) LightData { + glm::vec3 pointPosition; + float pointIntensity; + glm::vec3 ambientColor; + float ambientIntensity; + }; + static_assert(offsetof(LightData, pointIntensity) == 12); + static_assert(offsetof(LightData, ambientColor) == 16); + static_assert(offsetof(LightData, ambientIntensity) == 28); + static_assert(sizeof(LightData) == 32); + + struct alignas(16) SolidColorData { + glm::vec4 color; + }; + static_assert(sizeof(SolidColorData) == 16); + + // For simplicity, the tutorial hardcodes three shaders. + enum class ShaderVariant : std::uint32_t { + AlbedoAndNormal, + Albedo, + SolidColor, + }; + + // We store the data used to draw a renderable object. + // We store our data in vectors and store indices into those vectors. + // This simplifies reallocation and requires less memory, as an index is smaller than a pointer. + struct SceneDraw { + std::uint32_t m_meshId = 0; + std::uint32_t m_objectIndex = 0; + ShaderVariant m_shaderVariant = ShaderVariant::AlbedoAndNormal; + std::uint32_t m_albedoTextureIndex = util::gltf::invalidGltfId; + std::uint32_t m_normalTextureIndex = util::gltf::invalidGltfId; + std::string m_debugName; + }; + + struct GpuMesh { + GpuBuffer m_vertices{}; + GpuBuffer m_indices{}; + std::uint32_t m_indexCount = 0; + }; + + struct SceneResources { + // Geometry, object transforms, and textures used by scene draws. + GpuBuffer m_objects{}; + GpuBuffer m_pointLight{}; + GpuBuffer m_solidColor{}; + std::vector m_meshes{}; + std::vector m_textures{}; + std::vector m_drawData{}; + }; + + // Note: To save memory some applications might consider storing alignments, offsets and sizes as uint32_t + struct DescriptorHeapResources { + + // Wrapper to handle the data of a descriptor heap buffer. + // Helps reuse code across the sampler and descriptor heaps. + struct DescriptorHeapData { + GpuBuffer m_buffer; + + // Bound heap ranges must start at device-aligned GPU addresses. This offset + // moves from the buffer's base address to the aligned heap base used by Vulkan. + VkDeviceSize m_bindOffset = 0; + + // Size of the heap. + VkDeviceSize m_rangeSize = 0; + + // Obtained from device properties during device creation. + // These values are only needed for allocating and binding heap ranges. + // We keep them as part of the heap data for simplicity. + // Note: the resource and sampler heaps can have different requirements. + // + // GPU-address alignment requirement for the bound heap range. + VkDeviceSize m_alignment = 0; + // Minimum implementation-reserved range size for heap operations. + VkDeviceSize m_minReservedRange = 0; + }; + + // Host-visible buffers that contain raw descriptor bytes for EXT_descriptor_heap. + DescriptorHeapData m_resourceHeap{}; + DescriptorHeapData m_samplerHeap{}; + + // Descriptor sizes and heap alignments are device properties, so they are queried at startup. + VkDeviceSize m_sampledImageSize = 0; + VkDeviceSize m_samplerSize = 0; + VkDeviceSize m_uniformBufferSize = 0; + VkDeviceSize m_storageBufferSize = 0; + VkDeviceSize m_imageDescriptorAlignment = 0; + VkDeviceSize m_bufferDescriptorAlignment = 0; + VkDeviceSize m_samplerDescriptorAlignment = 0; + VkDeviceSize m_maxPushDataSize = 0; + std::uint32_t m_uniformBufferStride = 0; + std::uint32_t m_sampledImageStride = 0; + + // Byte offsets of each descriptor inside the heap buffers. + std::uint32_t m_cameraOffset = 0; + std::uint32_t m_objectsOffset = 0; + std::uint32_t m_pointLightOffset = 0; + std::uint32_t m_solidColorOffset = 0; + std::uint32_t m_pushTextureOffset = 0; + std::uint32_t m_linearSamplerOffset = 0; + std::uint32_t m_nearestSamplerOffset = 0; + }; + + struct ShaderGroup { + vk::UniqueShaderEXT m_vertex{}; + vk::UniqueShaderEXT m_fragment{}; + }; + + //**************** + // Vulkan initialization + //**************** + + void initGLFWWindow(); + + void initVulkanVKB(); + + void initCommandPool(); + + void initFramesInFlightResources(); + + void initSwapchainImageSyncObjects(); + + void initDepthImages(); + + //**************** + // Scene initialization + //**************** + + void initSceneResources(); + + void initSceneMeshesAndDrawData(const util::gltf::ParsedData& gltfData, std::vector& objects); + + void initSceneCamera(const util::gltf::ParsedData& gltfData); + + void initObjectBuffer(std::span objects); + + void initPointLight(const util::gltf::ParsedData& gltfData); + + void initSolidColor(); + + void initSceneTextures(const util::gltf::ParsedData& gltfData); + + //**************** + // Shader and descriptor setup + //**************** + + void initDescriptorHeaps(); + + [[nodiscard]] std::vector buildShaderDescriptorMappings( + const std::unordered_map& shaderResourceBindings) const; + + void calculateVertexInputs(); + + void initShaderObjects(); + + ShaderGroup createShaderGroup(const ShaderFilePair& shaderFilePair); + + //**************** + // Runtime loop + //**************** + + void mainLoop(); + + void updateCamera(float deltaSeconds); + + void renderFrame(); + + void waitForFrameResources(FrameInFlightResources& frame); + + std::uint32_t acquireSwapchainImage(FrameInFlightResources& frame); + + void uploadCameraData(FrameInFlightResources& frame); + + void startRecordingCommandBuffer(FrameInFlightResources& frame); + + void recordRenderingCommandBuffer(FrameInFlightResources& frame, std::uint32_t frameIndex, + std::uint32_t swapchainImageIndex); + void finishAndSubmitMainCommandBuffer(FrameInFlightResources& frame, std::uint32_t swapchainImageIndex); + + //**************** + // Helper command buffer utils + //**************** + + [[nodiscard]] vk::CommandBuffer getHelperCommandBuffer() const; + + void beginHelperCommands(); + + void endHelperCommandsAndFlushUploads(); + + //**************** + // Destruction and cleanup + //**************** + + void cleanup(); + + //**************** + // Resource utils + //**************** + + [[nodiscard]] std::uint32_t findMemoryType(std::uint32_t typeBits, vk::MemoryPropertyFlags properties) const; + [[nodiscard]] GpuBuffer createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags memoryProperties, std::string_view debugName) const; + [[nodiscard]] GpuBuffer uploadToNewStagingBuffer(std::span data, std::string_view debugName) const; + [[nodiscard]] GpuBuffer uploadToNewGpuBuffer(std::span data, vk::BufferUsageFlags finalUsage, + std::string_view debugName); + void uploadBuffer(const GpuBuffer& buffer, std::span data) const; + void allocateGpuImage(GpuImage& image) const; + [[nodiscard]] GpuImage createTexture(const util::ImageRgba8& image, std::string_view debugName); + + template + void setDebugName(Handle handle, vk::ObjectType objectType, const std::string& name) const; + static void beginDebugLabel(vk::CommandBuffer commandBuffer, const std::string& name, + const std::array& color); + static void endDebugLabel(vk::CommandBuffer commandBuffer); + + static void transitionImage(vk::CommandBuffer commandBuffer, vk::Image image, ImageState& currentState, + const ImageState& newState, vk::ImageSubresourceRange subresourceRange); + //**************** + // Data + //**************** + + // GLFW owns the platform window; Vulkan owns the surface created from it. + GLFWwindow* m_window = nullptr; + + // vk-bootstrap metadata needed for cleanup during destruction. + struct { + vkb::Instance m_instance{}; + vkb::Device m_device{}; + vkb::Swapchain m_swapchain{}; + } m_vkbData; + + vk::SurfaceKHR m_surface{}; + vk::Device m_logicalDevice{}; + std::vector m_memoryTypeFlags{}; + + vk::Queue m_graphicsQueue{}; + vk::Queue m_presentQueue{}; + std::uint32_t m_graphicsQueueFamily = 0; + + vk::Format m_swapchainFormat{}; + vk::Extent2D m_swapchainExtent{}; + std::vector m_swapchainImages; + + // Owns the command buffers used by the frame resources. + // UniqueCommandPool releases it automatically. + vk::UniqueCommandPool m_commandPool{}; + HelperCommandBuffer m_helperCommandBuffer{}; + std::vector m_pendingUploadStagingBuffers{}; + + // Two frames in flight provide useful overlap without much extra memory usage. + static constexpr std::uint32_t maxFramesInFlight = 2; + std::array m_framesInFlight{}; + + std::uint32_t m_currentFrameInFlight = 0; + + // Number of frames before the application will close itself. Useful for automated testing. + // 0 for unlimited frames. + std::uint32_t m_remainingFrameLimit = 0; + + struct VertexInput { + std::vector m_vertexBindings{}; + std::vector m_vertexAttributes{}; + }; + VertexInput m_vertexInput; + + struct ShaderObjects { + ShaderGroup m_albedoAndNormal{}; + ShaderGroup m_albedo{}; + ShaderGroup m_solidColor{}; + }; + ShaderObjects m_shaderObjects; + + struct ShaderBinaryCacheProperties { + std::array m_shaderBinaryUUID{}; + std::uint32_t m_shaderBinaryVersion = 0; + }; + ShaderBinaryCacheProperties m_shaderBinaryCacheProperties{}; + + SceneResources m_scene{}; + DescriptorHeapResources m_descriptorHeaps{}; + + glm::vec3 m_cameraPos{}; + float m_cameraYaw = 0.0F; + float m_cameraPitch = 0.0F; + + //******* + // Constant values used to configure the application. + // ******* + static constexpr float cameraPitchLimit = glm::radians(89.0F); + + // The tutorial has a fixed window size and does not handle resizing. + // Resize is not implemented because it requires recreating the swapchain and all related resources. + static constexpr int windowWidth = 1280; + static constexpr int windowHeight = 720; + + static struct DebugData { + // This enables adding debug names and labels, gates validation layers, and enables other debug features. + static constexpr bool enableGpuDebug = true; + + // These colors are used for debug markers. + // This allows us to visually identify which part of the frame corresponds to each call. + static constexpr std::array frameColor{0.18F, 0.34F, 0.78F, 1.0F}; + static constexpr std::array setupColor{0.62F, 0.42F, 0.16F, 1.0F}; + static constexpr std::array transferColor{0.78F, 0.42F, 0.18F, 1.0F}; + static constexpr std::array renderColor{0.18F, 0.55F, 0.35F, 1.0F}; + static constexpr std::array drawColor{0.62F, 0.24F, 0.72F, 1.0F}; + static constexpr std::array barrierColor{0.48F, 0.48F, 0.48F, 1.0F}; + + } debugData; + + // Store depth/color formats and image layouts for easy reuse. + static constexpr vk::Format depthFormat = vk::Format::eD32Sfloat; + static constexpr vk::Format mainTextureFormat = vk::Format::eR8G8B8A8Unorm; + + static constexpr vk::ImageLayout colorAttachmentLayout = vk::ImageLayout::eAttachmentOptimal; + static constexpr vk::ImageLayout depthAttachmentLayout = vk::ImageLayout::eAttachmentOptimal; + + // We render directly to the swapchain images. + static constexpr vk::PipelineStageFlags2 swapchainAcquireWaitStage = + vk::PipelineStageFlagBits2::eColorAttachmentOutput; + + // Helper to initialize component mapping with identity swizzles. + static constexpr vk::ComponentMapping identityComponentMapping{ + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity, + }; + +}; // class Application + +// Note: a normal application might generate shader-specific structs +// like Vertex or DescriptorHeapDrawPushIndicesAlbedoAndNormal from shader reflection data, +// usually with a build-time script that reads the Slang JSON and emits C++ code. +using Vertex = util::PackedVertex; + +// Push data is small per-draw data sent directly into the command buffer. +// Here it selects the frame camera descriptor and the per-material combined sampled images. +// Texture fields use the combined image-sampler index layout expected by VK_EXT_descriptor_heap. +// There are multiple ways to send data using descriptor heaps. +// We send indices to select descriptors, but we could also send raw data directly. +struct DescriptorHeapDrawPushIndicesAlbedoAndNormal { + std::uint32_t cameraIndex; + std::uint32_t albedoTextureIndex; + std::uint32_t normalTextureIndex; +}; + +struct DescriptorHeapDrawPushIndicesAlbedo { + std::uint32_t cameraIndex; + std::uint32_t albedoTextureIndex; +}; + +struct DescriptorHeapDrawPushIndicesSolidColor { + std::uint32_t cameraIndex; +}; + +// We use the offsets to link mappings with the push data. +static_assert(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, cameraIndex) == 0); +static_assert(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, albedoTextureIndex) == sizeof(std::uint32_t)); +static_assert(offsetof(DescriptorHeapDrawPushIndicesAlbedoAndNormal, normalTextureIndex) == sizeof(std::uint32_t) * 2U); + +static_assert(offsetof(DescriptorHeapDrawPushIndicesAlbedo, cameraIndex) == 0); +static_assert(offsetof(DescriptorHeapDrawPushIndicesAlbedo, albedoTextureIndex) == sizeof(std::uint32_t)); +static_assert(offsetof(DescriptorHeapDrawPushIndicesSolidColor, cameraIndex) == 0); +} // namespace siggraph diff --git a/attachments/siggraph2026_vk_tutorial/src/util.cpp b/attachments/siggraph2026_vk_tutorial/src/util.cpp new file mode 100644 index 00000000..33acdaca --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/src/util.cpp @@ -0,0 +1,1471 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// This file contains helper functions to make the Vulkan code in main.cpp easier to understand +// The idea is to abstract concepts like glTF parsing that are not relevant for a Vulkan application +// We suggest that people learning Vulkan ignore this file. + +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// stb_image and tinygltf are header-only libraries. These macros make this one +// translation unit provide their implementation code for the whole program. +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ONLY_PNG +#define STBI_ONLY_JPEG +#include + +#define TINYGLTF_IMPLEMENTATION +#define TINYGLTF_NO_STB_IMAGE +#define TINYGLTF_NO_STB_IMAGE_WRITE +#define TINYGLTF_NO_EXTERNAL_IMAGE +#include + +namespace siggraph { +namespace util { +namespace { + +// Everything in this anonymous namespace is private to util.cpp. +// The public beginner-facing helpers are declared in util.h; +// these helpers keep their implementations small enough to read. + +// Slang reflection names scalar/vector types such as float32x3. +// Vulkan needs the matching VkFormat so it knows how to fetch each vertex attribute from memory. +[[nodiscard]] vk::Format vertexFormatFromSlangType(std::string_view scalarType, int elementCount) +{ + if (scalarType == "float32") { + switch (elementCount) { + case 1: + return vk::Format::eR32Sfloat; + case 2: + return vk::Format::eR32G32Sfloat; + case 3: + return vk::Format::eR32G32B32Sfloat; + case 4: + return vk::Format::eR32G32B32A32Sfloat; + default: + break; + } + } + + if (scalarType == "uint32") { + switch (elementCount) { + case 1: + return vk::Format::eR32Uint; + case 2: + return vk::Format::eR32G32Uint; + case 3: + return vk::Format::eR32G32B32Uint; + case 4: + return vk::Format::eR32G32B32A32Uint; + default: + break; + } + } + + throw std::runtime_error(std::format("Unsupported Slang vertex input type: {}x{}", scalarType, elementCount)); +} + +// Reflection describes shader resources in Slang terms. +// Vulkan shader objects validate descriptor bindings with VkSpirvResourceTypeFlagsEXT masks instead. +// We use this function to convert between the two. +[[nodiscard]] VkSpirvResourceTypeFlagsEXT resourceMaskFromSlangType(const nlohmann::json& type) +{ + const std::string kind = type.value("kind", std::string{}); + if (kind == "constantBuffer") { + return VK_SPIRV_RESOURCE_TYPE_UNIFORM_BUFFER_BIT_EXT; + } + if (kind == "samplerState") { + return VK_SPIRV_RESOURCE_TYPE_SAMPLER_BIT_EXT; + } + + if (kind == "resource") { + const std::string baseShape = type.value("baseShape", std::string{}); + if (baseShape == "structuredBuffer") { + return VK_SPIRV_RESOURCE_TYPE_READ_ONLY_STORAGE_BUFFER_BIT_EXT; + } + if (baseShape == "texture2D") { + if (type.value("combined", false)) { + return VK_SPIRV_RESOURCE_TYPE_COMBINED_SAMPLED_IMAGE_BIT_EXT; + } + return VK_SPIRV_RESOURCE_TYPE_SAMPLED_IMAGE_BIT_EXT; + } + } + + throw std::runtime_error(std::format("Unsupported Slang reflected resource type: {}", kind)); +} + +[[nodiscard]] float sRgbChannelToLinear(std::uint8_t value) +{ + // Convert one 8-bit display-encoded channel into a linear value. + // sRGB transfer function: + // https://www.w3.org/TR/css-color-4/#predefined-sRGB + // https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#_srgb_transfer_functions + const float normalized = static_cast(value) / 255.0F; + if (normalized <= 0.04045F) { + return normalized / 12.92F; + } + return std::pow((normalized + 0.055F) / 1.055F, 2.4F); +} + +// glTF is a Khronos Group format to efficiently store 3D data. +// We are using tinygltf to parse the current object to read it +// +// glTF stores raw bytes in buffers. +// A buffer view is a contiguous slice of one buffer, often holding one vertex or index stream. +// An accessor describes how to interpret bytes inside a buffer view: scalar/vector shape, component type, element +// count, offset, and optional stride. + +// tinygltf stores glTF object references as signed indices, using -1 for optional missing references. +// Normalize those values at the parser boundary so internal parsed IDs and validated indices stay uint32_t. +[[nodiscard]] std::uint32_t optionalGltfIndex(int index) +{ + return index < 0 ? gltf::invalidGltfId : safeCastToU32(index); +} + +[[nodiscard]] std::uint32_t requiredGltfIndex(int index, std::string_view what) +{ + require(index >= 0, "glTF " + std::string(what) + " index is missing"); + return safeCastToU32(index); +} + +// These wrappers centralize bounds checks so the parsing code can stay readable. +[[nodiscard]] const tinygltf::Accessor& gltfAccessorAt(const tinygltf::Model& model, std::uint32_t index, + std::string_view what) +{ + require(index != gltf::invalidGltfId && index < model.accessors.size(), + "glTF " + std::string(what) + " accessor index is out of range"); + return model.accessors[index]; +} + +[[nodiscard]] const tinygltf::BufferView& gltfBufferViewAt(const tinygltf::Model& model, std::uint32_t index, + std::string_view what) +{ + require(index != gltf::invalidGltfId && index < model.bufferViews.size(), + "glTF " + std::string(what) + " buffer view index is out of range"); + return model.bufferViews[index]; +} + +[[nodiscard]] const tinygltf::Buffer& gltfBufferAt(const tinygltf::Model& model, std::uint32_t index, + std::string_view what) +{ + require(index != gltf::invalidGltfId && index < model.buffers.size(), + "glTF " + std::string(what) + " buffer index is out of range"); + return model.buffers[index]; +} + +[[nodiscard]] std::size_t gltfAccessorByteSize(std::size_t elementCount, std::size_t byteStride, + std::size_t elementSize, std::string_view what) +{ + if (elementCount == 0) { + return 0; + } + + const std::size_t lastElementIndex = elementCount - 1U; + require(byteStride == 0 || lastElementIndex <= (std::numeric_limits::max() - elementSize) / byteStride, + "glTF " + std::string(what) + " accessor byte range overflows size_t"); + return lastElementIndex * byteStride + elementSize; +} + +void requireGltfAccessorByteRange(std::span accessorBytes, std::size_t byteOffset, + std::size_t byteSize, std::string_view what) +{ + require(byteOffset <= accessorBytes.size() && byteSize <= accessorBytes.size() - byteOffset, + "glTF " + std::string(what) + " accessor byte span read is out of bounds"); +} + +[[nodiscard]] std::span gltfAccessorData(const tinygltf::Model& model, + const tinygltf::Accessor& accessor, + const tinygltf::BufferView& bufferView, + std::size_t elementSize, std::string_view what) +{ + // This tutorial handles normal, dense accessors only. + // Sparse accessors are a valid glTF feature, but supporting them would obscure the basic data path. + require(!accessor.sparse.isSparse, "glTF sparse accessors are not supported"); + + const std::uint32_t bufferIndex = requiredGltfIndex(bufferView.buffer, "buffer"); + const tinygltf::Buffer& buffer = gltfBufferAt(model, bufferIndex, what); + + const std::size_t byteOffset = bufferView.byteOffset + accessor.byteOffset; + require(byteOffset <= buffer.data.size(), "glTF " + std::string(what) + " accessor starts past its buffer"); + + const int stride = accessor.ByteStride(bufferView); + require(stride > 0, "glTF " + std::string(what) + " accessor has invalid byte stride"); + + require(static_cast(stride) >= elementSize, + "glTF " + std::string(what) + " accessor stride is smaller than one element"); + + // The final element may be strided, so validate the address of the last element plus its own byte size. + const std::size_t byteSize = + gltfAccessorByteSize(accessor.count, static_cast(stride), elementSize, what); + require(byteSize <= buffer.data.size() - byteOffset, + "glTF " + std::string(what) + " accessor reads past its buffer"); + + return std::span{reinterpret_cast(buffer.data.data() + byteOffset), byteSize}; +} + +template +[[nodiscard]] std::vector copyGltfAccessorElements(const tinygltf::Accessor& accessor, + const tinygltf::BufferView& bufferView, + std::span accessorBytes) +{ + // glTF attributes may be tightly packed or interleaved with other attributes. + // ByteStride tells us how far to jump from one element to the next. + const std::size_t byteStride = static_cast(accessor.ByteStride(bufferView)); + require(byteStride > 0, "glTF accessor has invalid byte stride"); + require(byteStride >= sizeof(Value), "glTF accessor stride is smaller than one element"); + + std::vector values(accessor.count); + const std::size_t requiredByteSize = gltfAccessorByteSize(values.size(), byteStride, sizeof(Value), "attribute"); + require(accessorBytes.size() >= requiredByteSize, "glTF accessor byte span is smaller than the requested data"); + if (values.empty()) { + return values; + } + + if (byteStride == sizeof(Value)) { + // Tightly packed accessors can be copied in one block. + requireGltfAccessorByteRange(accessorBytes, 0, requiredByteSize, "attribute"); + std::memcpy(values.data(), accessorBytes.data(), requiredByteSize); + return values; + } + + for (std::size_t i = 0; i < values.size(); ++i) { + const std::size_t byteOffset = i * byteStride; + requireGltfAccessorByteRange(accessorBytes, byteOffset, sizeof(Value), "attribute"); + // memcpy avoids alignment assumptions about the byte buffer returned by tinygltf. + std::memcpy(&values[i], accessorBytes.data() + byteOffset, sizeof(Value)); + } + return values; +} + +template +[[nodiscard]] std::vector readGltfFloatVectorAccessor(const tinygltf::Model& model, std::uint32_t accessorIndex, + int expectedAccessorType, + std::string_view expectedTypeName, std::string_view what) +{ + // Positions, normals, tangents, and UVs are all float vectors in this sample. + // The template keeps their validation and copy logic in one place. + const tinygltf::Accessor& accessor = gltfAccessorAt(model, accessorIndex, what); + require(accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT, + "glTF " + std::string(what) + " accessor must contain float components"); + require(accessor.type == expectedAccessorType, + "glTF " + std::string(what) + " accessor must be " + std::string(expectedTypeName)); + + const std::uint32_t bufferViewIndex = requiredGltfIndex(accessor.bufferView, std::string(what) + " buffer view"); + const tinygltf::BufferView& bufferView = gltfBufferViewAt(model, bufferViewIndex, what); + const std::span accessorBytes = gltfAccessorData(model, accessor, bufferView, sizeof(Value), what); + return copyGltfAccessorElements(accessor, bufferView, accessorBytes); +} + +[[nodiscard]] std::vector readGltfVec3Accessor(const tinygltf::Model& model, std::uint32_t accessorIndex, + std::string_view what) +{ + return readGltfFloatVectorAccessor(model, accessorIndex, TINYGLTF_TYPE_VEC3, "VEC3", what); +} + +[[nodiscard]] std::vector readGltfVec4Accessor(const tinygltf::Model& model, std::uint32_t accessorIndex, + std::string_view what) +{ + return readGltfFloatVectorAccessor(model, accessorIndex, TINYGLTF_TYPE_VEC4, "VEC4", what); +} + +[[nodiscard]] std::vector readGltfVec2Accessor(const tinygltf::Model& model, std::uint32_t accessorIndex, + std::string_view what) +{ + return readGltfFloatVectorAccessor(model, accessorIndex, TINYGLTF_TYPE_VEC2, "VEC2", what); +} + +[[nodiscard]] std::size_t gltfIndexElementSize(int componentType) +{ + // glTF allows several integer widths for index buffers. + // The renderer expands all of them to uint32_t so the rest of the tutorial has one index type. + switch (componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + return sizeof(std::uint8_t); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return sizeof(std::uint16_t); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return sizeof(std::uint32_t); + default: + throw std::runtime_error(std::format("glTF index accessor uses unsupported component type {}", componentType)); + } +} + +template +[[nodiscard]] std::vector copyGltfIndexElements(const tinygltf::Accessor& accessor, + const tinygltf::BufferView& bufferView, + std::span accessorBytes) +{ + const std::size_t byteStride = static_cast(accessor.ByteStride(bufferView)); + require(byteStride > 0, "glTF index accessor has invalid byte stride"); + require(byteStride >= sizeof(StoredIndex), "glTF index accessor stride is smaller than one element"); + + std::vector values(accessor.count); + const std::size_t requiredByteSize = gltfAccessorByteSize(values.size(), byteStride, sizeof(StoredIndex), "index"); + require(accessorBytes.size() >= requiredByteSize, "glTF index accessor byte span is smaller than the index data"); + if (values.empty()) { + return values; + } + + if (byteStride == sizeof(StoredIndex)) { + // Tightly packed index accessors can be read as one byte range. + requireGltfAccessorByteRange(accessorBytes, 0, requiredByteSize, "index"); + if constexpr (std::is_same_v) { + std::memcpy(values.data(), accessorBytes.data(), requiredByteSize); + } + else { + for (std::size_t i = 0; i < values.size(); ++i) { + StoredIndex value = 0; + std::memcpy(&value, accessorBytes.data() + i * sizeof(StoredIndex), sizeof(value)); + values[i] = static_cast(value); + } + } + return values; + } + + // Interleaved index buffers are unusual, but glTF allows them through byteStride. + for (std::size_t i = 0; i < values.size(); ++i) { + const std::size_t byteOffset = i * byteStride; + requireGltfAccessorByteRange(accessorBytes, byteOffset, sizeof(StoredIndex), "index"); + StoredIndex value = 0; + std::memcpy(&value, accessorBytes.data() + byteOffset, sizeof(value)); + values[i] = static_cast(value); + } + return values; +} + +[[nodiscard]] std::vector readGltfIndexAccessor(const tinygltf::Model& model, + std::uint32_t accessorIndex) +{ + // Index accessors are scalar integer streams. They point into the same glTF + // buffer/bufferView system as vertex attributes. + const tinygltf::Accessor& accessor = gltfAccessorAt(model, accessorIndex, "index"); + require(accessor.type == TINYGLTF_TYPE_SCALAR, "glTF index accessor must be SCALAR"); + + const std::size_t elementSize = gltfIndexElementSize(accessor.componentType); + const std::uint32_t bufferViewIndex = requiredGltfIndex(accessor.bufferView, "index buffer view"); + const tinygltf::BufferView& bufferView = gltfBufferViewAt(model, bufferViewIndex, "index"); + const std::span accessorBytes = + gltfAccessorData(model, accessor, bufferView, elementSize, "index"); + + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + return copyGltfIndexElements(accessor, bufferView, accessorBytes); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return copyGltfIndexElements(accessor, bufferView, accessorBytes); + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return copyGltfIndexElements(accessor, bufferView, accessorBytes); + default: + throw std::runtime_error( + std::format("glTF index accessor uses unsupported component type {}", accessor.componentType)); + } +} + +[[nodiscard]] glm::mat4 gltfNodeLocalTransform(const tinygltf::Node& node) +{ + // glTF nodes can either store a full 4x4 matrix or separate translation, + // rotation, and scale values. A matrix takes precedence when present. + if (!node.matrix.empty()) { + require(node.matrix.size() == 16, "glTF node matrix must contain 16 values"); + glm::mat4 transform{1.0F}; + // glTF stores matrices in column-major order, which matches GLM's matrix indexing. + for (glm::length_t column = 0; column < 4; ++column) { + for (glm::length_t row = 0; row < 4; ++row) { + const std::size_t matrixIndex = static_cast(column) * 4U + static_cast(row); + transform[column][row] = static_cast(node.matrix[matrixIndex]); + } + } + return transform; + } + + glm::vec3 translation{0.0F}; + if (!node.translation.empty()) { + require(node.translation.size() == 3, "glTF node translation must contain 3 values"); + translation = glm::vec3{static_cast(node.translation[0]), static_cast(node.translation[1]), + static_cast(node.translation[2])}; + } + + glm::quat rotation{1.0F, 0.0F, 0.0F, 0.0F}; + if (!node.rotation.empty()) { + require(node.rotation.size() == 4, "glTF node rotation must contain 4 values"); + // glTF writes quaternions as x,y,z,w; GLM's constructor takes w,x,y,z. + rotation = + glm::normalize(glm::quat{static_cast(node.rotation[3]), static_cast(node.rotation[0]), + static_cast(node.rotation[1]), static_cast(node.rotation[2])}); + } + + glm::vec3 scale{1.0F}; + if (!node.scale.empty()) { + require(node.scale.size() == 3, "glTF node scale must contain 3 values"); + scale = glm::vec3{static_cast(node.scale[0]), static_cast(node.scale[1]), + static_cast(node.scale[2])}; + } + + // glTF composes local transforms in translation * rotation * scale order. + return glm::translate(glm::mat4{1.0F}, translation) * glm::mat4_cast(rotation) * glm::scale(glm::mat4{1.0F}, scale); +} + +// A scene may draw the same mesh data from multiple nodes. +// This key lets us store one copy of the shared geometry and let each node reference it by ID. +struct GeometryKey { + std::uint32_t positionId = gltf::invalidGltfId; + std::uint32_t indexId = gltf::invalidGltfId; + std::uint32_t uvId = gltf::invalidGltfId; + std::uint32_t normalId = gltf::invalidGltfId; + std::uint32_t tangentId = gltf::invalidGltfId; + + [[nodiscard]] bool operator==(const GeometryKey& other) const = default; +}; + +struct GeometryKeyHash { + [[nodiscard]] std::size_t operator()(const GeometryKey& key) const + { + return combineHash(key.positionId, key.indexId, key.uvId, key.normalId, key.tangentId); + } +}; + +[[nodiscard]] glm::vec3 fallbackTangentForNormal(const glm::vec3& normal) +{ + // Pick any helper axis that is not almost parallel to the normal, + // then cross it with the normal to get a stable perpendicular tangent. + const glm::vec3 helper = std::abs(normal.y) < 0.999F ? glm::vec3{0.0F, 1.0F, 0.0F} : glm::vec3{1.0F, 0.0F, 0.0F}; + return glm::normalize(glm::cross(helper, normal)); +} + +void generateMissingTangents(MeshGeometryData& geometry) +{ + constexpr float tangentEpsilon = 0.000001F; + + // Normal mapping needs a tangent basis. + // When a model has UVs and normals but no tangents, + // accumulate one tangent/bitangent estimate per triangle. + std::vector tangentSums(geometry.vertices.size(), glm::vec3{0.0F}); + std::vector bitangentSums(geometry.vertices.size(), glm::vec3{0.0F}); + + for (std::size_t i = 0; i + 2 < geometry.indices.size(); i += 3) { + const std::uint32_t i0 = geometry.indices[i + 0]; + const std::uint32_t i1 = geometry.indices[i + 1]; + const std::uint32_t i2 = geometry.indices[i + 2]; + + const PackedVertex& v0 = geometry.vertices[i0]; + const PackedVertex& v1 = geometry.vertices[i1]; + const PackedVertex& v2 = geometry.vertices[i2]; + + const glm::vec3 edge1 = v1.position - v0.position; + const glm::vec3 edge2 = v2.position - v0.position; + const glm::vec2 deltaUv1 = v1.uv - v0.uv; + const glm::vec2 deltaUv2 = v2.uv - v0.uv; + const float determinant = deltaUv1.x * deltaUv2.y - deltaUv1.y * deltaUv2.x; + if (std::abs(determinant) <= tangentEpsilon) { + // Degenerate UV triangles cannot define a useful tangent direction. + continue; + } + + const float inverseDeterminant = 1.0F / determinant; + const glm::vec3 tangent = (edge1 * deltaUv2.y - edge2 * deltaUv1.y) * inverseDeterminant; + const glm::vec3 bitangent = (edge2 * deltaUv1.x - edge1 * deltaUv2.x) * inverseDeterminant; + + tangentSums[i0] += tangent; + tangentSums[i1] += tangent; + tangentSums[i2] += tangent; + bitangentSums[i0] += bitangent; + bitangentSums[i1] += bitangent; + bitangentSums[i2] += bitangent; + } + + // Average the triangle estimates at each vertex and make the tangent + // perpendicular to the normal with a Gram-Schmidt step. + for (std::size_t i = 0; i < geometry.vertices.size(); ++i) { + const glm::vec3 normal = glm::normalize(geometry.vertices[i].normal); + glm::vec3 tangent = tangentSums[i] - normal * glm::dot(normal, tangentSums[i]); + if (glm::dot(tangent, tangent) <= tangentEpsilon) { + tangent = fallbackTangentForNormal(normal); + } + else { + tangent = glm::normalize(tangent); + } + + const float handedness = glm::dot(glm::cross(normal, tangent), bitangentSums[i]) < 0.0F ? -1.0F : 1.0F; + // Store xyz as the tangent direction and w as handedness so the shader can reconstruct the bitangent. + geometry.vertices[i].tangent = glm::vec4{tangent, handedness}; + } +} + +[[nodiscard]] gltf::Node makeParsedNode(const glm::mat4& transform) +{ + // ParsedData stores transform components because main.cpp later builds model + // matrices from position, Euler rotation, and scale sliders/values. + gltf::Node parsedNode{}; + parsedNode.pos = glm::vec3{transform[3]}; + parsedNode.scale = glm::vec3{glm::length(glm::vec3{transform[0]}), glm::length(glm::vec3{transform[1]}), + glm::length(glm::vec3{transform[2]})}; + + // Remove scale from the matrix columns before converting the remaining + // rotation matrix into Euler angles. + glm::mat3 rotationMatrix{1.0F}; + for (glm::length_t column = 0; column < 3; ++column) { + const float axisScale = parsedNode.scale[column]; + if (axisScale > 0.0F) { + rotationMatrix[column] = glm::vec3{transform[column]} / axisScale; + } + } + parsedNode.eulerAngles = glm::eulerAngles(glm::quat_cast(rotationMatrix)); + return parsedNode; +} + +[[nodiscard]] std::string gltfNameOrFallback(const std::string& name, std::string_view fallbackPrefix, + std::uint32_t index) +{ + // Names from the file are best for debugging; + // generated names keep error messages useful when an asset omits them. + if (!name.empty()) { + return name; + } + return std::string{fallbackPrefix} + "[" + std::to_string(index) + "]"; +} + +[[nodiscard]] std::string gltfMeshPrimitiveName(const tinygltf::Mesh& mesh, std::uint32_t meshIndex, + std::uint32_t primitiveIndex, std::size_t primitiveCount) +{ + // A glTF mesh can have several primitives; include the primitive index only when it matters. + std::string name = gltfNameOrFallback(mesh.name, "Mesh", meshIndex); + if (primitiveCount > 1) { + name += "/Primitive[" + std::to_string(primitiveIndex) + "]"; + } + return name; +} + +[[nodiscard]] std::string gltfTextureName(const tinygltf::Texture& texture, const tinygltf::Image& image, + std::uint32_t textureIndex) +{ + // Prefer names closest to the material reference, then fall back to the image or a generated name. + if (!texture.name.empty()) { + return texture.name; + } + if (!image.name.empty()) { + return image.name; + } + return gltfNameOrFallback(std::string{}, "Texture", textureIndex); +} + +[[nodiscard]] std::uint32_t parsedDataIdFromSize(std::size_t size, std::string_view what) +{ + const std::uint32_t id = safeCastToU32(size); + require(id != gltf::invalidGltfId, std::format("glTF {} count uses the reserved invalid ID", what)); + return id; +} + +[[nodiscard]] std::uint32_t +textureIdFromTextureIndex(const tinygltf::Model& model, const std::filesystem::path& gltfBaseDir, + std::uint32_t textureIndex, gltf::ParsedData& parsedData, + std::unordered_map& imageToTextureId) +{ + // In glTF, materials reference textures, and textures reference images. + // This tutorial stores one ParsedData texture per image URI and reuses it by ID. + if (textureIndex == gltf::invalidGltfId) { + return gltf::invalidGltfId; + } + + require(textureIndex < model.textures.size(), "glTF texture index is out of range"); + + const tinygltf::Texture& texture = model.textures[textureIndex]; + + const std::uint32_t imageIndex = requiredGltfIndex(texture.source, "texture image"); + require(imageIndex < model.images.size(), "glTF texture image index is out of range"); + + const auto existingIt = imageToTextureId.find(imageIndex); + if (existingIt != imageToTextureId.end()) { + // Multiple materials can point at the same image; keep one texture record and reuse its ID. + return existingIt->second; + } + + // Image not seen before, add it to the images in the scene. + + const tinygltf::Image& image = model.images[imageIndex]; + require(!image.uri.empty(), "glTF texture image must use an external URI"); + const std::filesystem::path texturePath = + std::filesystem::path{image.uri}.is_absolute() ? std::filesystem::path{image.uri} : gltfBaseDir / image.uri; + + const std::uint32_t id = parsedDataIdFromSize(parsedData.textures.size(), "texture"); + parsedData.textures.push_back(gltf::GltfTexture{ + .name = gltfTextureName(texture, image, textureIndex), + .filename = texturePath.lexically_normal().string(), + }); + imageToTextureId.emplace(imageIndex, id); + return id; +} + +} // namespace + +// BinaryBuffer is used to create buffers whose data have alignment requirements. +BinaryBuffer::BinaryBuffer(size_t size, size_t alignment) : size(size) +{ + // Allocate a little extra storage so an aligned subrange can always fit inside the vector. + storage.resize(alignedAllocationSize(size, alignment)); + + // Store how many bytes to skip before the aligned payload starts. + const std::size_t storage_data = reinterpret_cast(storage.data()); + offset = alignUp(storage_data, alignment) - storage_data; + + // Validate the two invariants this helper promises to callers. + require(alignUp(storage_data, alignment) == reinterpret_cast(data()), + "Generated binary buffer is not aligned."); + require((data() + this->size) <= (storage.data() + storage.size()), "Generated binary buffer is overflowing"); +}; + +BinaryBuffer readSpirvFile(const std::filesystem::path& path) +{ + // Vulkan consumes shader modules as SPIR-V bytecode. + // Keeping this as raw bytes lets the caller pass the data straight into Vulkan create-info structs. + + // Shader objects expect SPIR-V data to be 4-byte aligned. + std::size_t spirvAlignment = 4; + + // readBinaryFile handles the filesystem details and gives us aligned storage. + std::optional binBuffer = readBinaryFile(path, spirvAlignment); + + if (!binBuffer.has_value()) { + throw std::runtime_error(std::format("Failed to read SPIR-V file: {}", path.string())); + } + + // SPIR-V is defined as an array of 32-bit words. A non-multiple-of-4 file is invalid. + if ((static_cast(binBuffer.value().size) % sizeof(std::uint32_t)) != 0U) { + throw std::runtime_error(std::format("SPIR-V file size is not word aligned: {}", path.string())); + } + + return std::move(binBuffer.value()); +} + +std::optional readBinaryFile(const std::filesystem::path& path, std::size_t alignment) +{ + // Missing cache files are not errors. + // Other filesystem failures still need to stop execution because they may hide permission or path problems. + std::error_code existsError; + if (!std::filesystem::exists(path, existsError)) { + if (existsError) { + throw std::runtime_error(std::format("Failed to query binary file: {}", path.string())); + } + return std::nullopt; + } + + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + throw std::runtime_error(std::format("Failed to open binary file: {}", path.string())); + } + + // Opening at the end lets tellg() report the whole file size before allocation. + const auto fileSize = file.tellg(); + if (fileSize < 0) { + throw std::runtime_error(std::format("Failed to query binary file size: {}", path.string())); + } + else if (fileSize == 0) { + log_msg("File: {} is empty", path.string()); + return std::nullopt; + } + BinaryBuffer binBuffer(fileSize, alignment); + + // Rewind after sizing the file and read the bytes into the aligned payload area. + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(binBuffer.data()), binBuffer.size); + + if (!file) { + throw std::runtime_error(std::format("Failed to read binary file: {}", path.string())); + } + + return binBuffer; +} + +void writeBinaryFile(const std::filesystem::path& path, std::span data) +{ + // Cache/output paths may point into generated folders that do not exist yet. + if (path.has_parent_path()) { + std::filesystem::create_directories(path.parent_path()); + } + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file) { + throw std::runtime_error(std::format("Failed to open binary file for writing: {}", path.string())); + } + + if (!data.empty()) { + // std::span keeps this helper flexible: callers can pass vectors, arrays, + // or other contiguous byte ranges without copying first. + file.write(reinterpret_cast(data.data()), static_cast(data.size())); + } + if (!file) { + throw std::runtime_error(std::format("Failed to write binary file: {}", path.string())); + } +} + +glm::vec3 sRgbToLinear(std::uint8_t r, std::uint8_t g, std::uint8_t b) +{ + // Apply the same sRGB transfer function independently to R, G, and B. + return glm::vec3{sRgbChannelToLinear(r), sRgbChannelToLinear(g), sRgbChannelToLinear(b)}; +} + +std::uint64_t combineHash(std::span data, std::uint64_t seed) +{ + constexpr std::uint64_t fnvPrime = 1099511628211ULL; + + // Prefix the content with its length so ["ab", "c"] does not hash the same + // way as ["a", "bc"] when several values are combined in sequence. + const std::uint64_t dataSize = static_cast(data.size()); + for (std::size_t i = 0; i < sizeof(dataSize); ++i) { + const std::uint8_t value = static_cast((dataSize >> (i * 8U)) & 0xFFU); + seed ^= value; + seed *= fnvPrime; + } + for (const std::uint8_t value : data) { + seed ^= value; + seed *= fnvPrime; + } + return seed; +} + +std::uint64_t combineHash(std::span data, std::uint64_t seed) +{ + // std::byte and uint8_t have the same storage, but the uint8_t overload does the actual FNV-style byte mixing. + return combineHash(std::span{reinterpret_cast(data.data()), data.size()}, + seed); +} + +std::uint64_t combineHash(std::string_view value, std::uint64_t seed) +{ + // Hash the bytes of the string view without requiring a null terminator. + const std::span bytes{reinterpret_cast(value.data()), value.size()}; + return combineHash(bytes, seed); +} + +namespace slang { + +std::unordered_map +calculateReflectionShaderResourceBindings(const std::filesystem::path& path) +{ + // The tutorial asks Slang to emit JSON reflection next to each compiled shader. + // This lets the C++ code discover descriptor sets/bindings instead of duplicating those numbers by hand. + std::ifstream file(path); + if (!file) { + throw std::runtime_error(std::format("Failed to open Slang reflection file: {}", path.string())); + } + + // Parse reflection data as JSON + const nlohmann::json reflection = nlohmann::json::parse(file, nullptr, true, true); + + // Store all bindings in a map + std::unordered_map bindings; + + for (const nlohmann::json& parameter : reflection.value("parameters", nlohmann::json::array())) { + const nlohmann::json& binding = parameter.value("binding", nlohmann::json::object()); + if (binding.value("kind", std::string{}) != "descriptorTableSlot") { + continue; + } + + bindings.emplace(parameter.at("name").get(), + ShaderResourceBinding{ + // Descriptor setId from [[vk::binding(bindingId, setId)]] in vertex/fragment shaders. + .set = binding.value("set", 0U), + // Descriptor bindingId from [[vk::binding(bindingId, setId)]]. + .binding = binding.at("index").get(), + .resourceMask = resourceMaskFromSlangType(parameter.at("type")), + }); + } + + if (bindings.empty()) { + throw std::runtime_error( + std::format("No shader resource bindings found in Slang reflection file: {}", path.string())); + } + + return bindings; +} + +std::unordered_map +collectShaderResourceBindings(std::span reflectionPaths) +{ + require(!reflectionPaths.empty(), "At least one shader reflection path is required"); + + // Vertex and fragment shaders can share descriptor names. + // Shared names must agree exactly so one descriptor layout can be used for the full pipeline. + std::unordered_map mergedBindings; + for (const std::filesystem::path& reflectionPath : reflectionPaths) { + for (const auto& [name, binding] : calculateReflectionShaderResourceBindings(reflectionPath)) { + const auto [it, inserted] = mergedBindings.emplace(name, binding); + if (!inserted) { + require(it->second.set == binding.set && it->second.binding == binding.binding && + it->second.resourceMask == binding.resourceMask, + "Slang reflection mismatch for shared resource binding: " + name); + } + } + } + + return mergedBindings; +} + +std::unordered_map calculateReflectionVertexInputs(const std::filesystem::path& path) +{ + // Vertex inputs are reflected separately from descriptors, because Vulkan needs them in dynamic vertex-input state + // instead of descriptor layouts. + std::ifstream file(path); + if (!file) { + throw std::runtime_error(std::format("Failed to open Slang reflection file: {}", path.string())); + } + + const nlohmann::json reflection = nlohmann::json::parse(file, nullptr, true, true); + + std::unordered_map inputs; + + // Slang writes vertex input struct fields under entryPoints[0].parameters[0].type.fields. + // Each field carries a varyingInput location plus scalar/vector type information. + if (reflection.contains("entryPoints") && !reflection.at("entryPoints").empty()) { + const nlohmann::json& entryPoint = reflection.at("entryPoints").at(0); + if (entryPoint.contains("parameters") && !entryPoint.at("parameters").empty()) { + const nlohmann::json& entryParameter = entryPoint.at("parameters").at(0); + if (entryParameter.contains("type") && entryParameter.at("type").contains("fields")) { + const nlohmann::json& fields = entryParameter.at("type").at("fields"); + for (const nlohmann::json& field : fields) { + const nlohmann::json& binding = field.value("binding", nlohmann::json::object()); + if (binding.value("kind", std::string{}) != "varyingInput") { + continue; + } + + const nlohmann::json& type = field.at("type"); + const nlohmann::json& elementType = + type.value("kind", std::string{}) == "vector" ? type.at("elementType") : type; + int elementCount = type.value("elementCount", 1); + const std::string scalarType = elementType.at("scalarType").get(); + + // Store by shader field name so the PackedVertex layout table + // below can match "position", "uv", "normal", and "tangent". + inputs.emplace(field.at("name").get(), + VertexInput{ + .location = binding.at("index").get(), + .format = vertexFormatFromSlangType(scalarType, elementCount), + }); + } + } + } + } + + if (inputs.empty()) { + throw std::runtime_error( + std::format("No vertex input variables found in Slang reflection file: {}", path.string())); + } + + return inputs; +} + +PackedVertexInputLayout calculatePackedVertexInputLayout(const std::filesystem::path& reflectionPath) +{ + // Vulkan needs one binding description for the buffer and one attribute description per shader input. + PackedVertexInputLayout layout{}; + layout.bindings = {vk::VertexInputBindingDescription2EXT{ + // App binding slot: set by vkCmdSetVertexInputEXT and filled by vkCmdBindVertexBuffers2. + .binding = 0, + // Byte size of one interleaved CPU vertex. + .stride = sizeof(PackedVertex), + // Advance to the next PackedVertex for each vertex. + .inputRate = vk::VertexInputRate::eVertex, + // No instancing divisor; every vertex uses the next element. + .divisor = 1, + }}; + + const std::unordered_map vertexInputs = calculateReflectionVertexInputs(reflectionPath); + + // Keep the CPU vertex struct and shader input names in one visible table. + // If the shader changes, this require() path explains which field no longer matches. + struct VertexField { + std::string_view name; + std::uint32_t offset; + }; + const std::array vertexFields{ + VertexField{.name = "position", .offset = safeCastToU32(offsetof(PackedVertex, position))}, + VertexField{.name = "uv", .offset = safeCastToU32(offsetof(PackedVertex, uv))}, + VertexField{.name = "normal", .offset = safeCastToU32(offsetof(PackedVertex, normal))}, + VertexField{.name = "tangent", .offset = safeCastToU32(offsetof(PackedVertex, tangent))}, + }; + + layout.attributes.reserve(vertexFields.size()); + for (const VertexField& vertexField : vertexFields) { + const std::string fieldName{vertexField.name}; + const auto inputIt = vertexInputs.find(fieldName); + require(inputIt != vertexInputs.end(), "Slang reflection is missing vertex input: " + fieldName); + + layout.attributes.emplace_back(vk::VertexInputAttributeDescription2EXT{ + // Vertex input location, read from [[vk::location(locationId)]] in the vertex shader. + .location = inputIt->second.location, + // App binding slot: set by vkCmdSetVertexInputEXT and filled by vkCmdBindVertexBuffers2. + .binding = 0, + // Vulkan format inferred from the reflected Slang field type. + .format = inputIt->second.format, + // Byte offset of this field inside PackedVertex. + .offset = vertexField.offset, + }); + } + + return layout; +} + +} // namespace slang + +ImageRgba8 readImageFileRgba8(const std::filesystem::path& path) +{ + // stb_image gives this tutorial a small, dependency-light path from image bytes to CPU RGBA pixels. + // Texture creation stays in main.cpp, where Vulkan code is easier to follow. + int width = 0; + int height = 0; + int sourceChannels = 0; + constexpr int requiredChannels = 4; + stbi_uc* decodedPixels = stbi_load(path.string().c_str(), &width, &height, &sourceChannels, requiredChannels); + if (decodedPixels == nullptr) { + throw std::runtime_error( + std::format("Failed to read image file: {} ({})", path.string(), stbi_failure_reason())); + } + + ImageRgba8 image{}; + try { + // Requesting four channels means stb_image converts RGB/gray input into + // the RGBA layout expected by VK_FORMAT_R8G8B8A8_UNORM. + require(width > 0 && height > 0, "Image dimensions must be positive"); + image.m_width = safeCastToU32(width); + image.m_height = safeCastToU32(height); + + const std::size_t byteCount = + static_cast(image.m_width) * static_cast(image.m_height) * requiredChannels; + image.m_pixels.resize(byteCount / sizeof(std::uint32_t)); + // m_pixels is a uint32_t vector for convenient Vulkan upload sizing, but + // the copied bytes remain ordered R, G, B, A. + std::memcpy(image.m_pixels.data(), decodedPixels, byteCount); + } + catch (...) { + // stbi_load uses malloc internally, so always release it before rethrowing. + stbi_image_free(decodedPixels); + throw; + } + + // The decoded pixels have been copied into image.m_pixels, so stb's temporary allocation can be released. + stbi_image_free(decodedPixels); + return image; +} + +namespace gltf { + +void appendGltfFile(const std::filesystem::path& path, ParsedData& parsedData) +{ + // tinygltf can load both binary .glb files and text .gltf files. + tinygltf::TinyGLTF loader; + tinygltf::Model model; + std::string error; + std::string warning; + const std::filesystem::path gltfBaseDir = path.parent_path(); + + // Choose the loader from the extension so tutorial assets can use either glTF container format. + const std::string extension = path.extension().string(); + const bool loaded = extension == ".glb" ? loader.LoadBinaryFromFile(&model, &error, &warning, path.string()) + : loader.LoadASCIIFromFile(&model, &error, &warning, path.string()); + if (!loaded) { + throw std::runtime_error(std::format("Failed to parse glTF file: {} {}", path.string(), error)); + } + + const std::size_t nodeCountBefore = parsedData.nodes.size(); + const bool hadCameraBefore = parsedData.hasCamera; + + // These maps turn glTF accessor/image indices into compact IDs in ParsedData. + // They also avoid copying shared buffers more than once. + std::unordered_map positionAccessorIds; + std::unordered_map uvAccessorIds; + std::unordered_map normalAccessorIds; + std::unordered_map tangentAccessorIds; + std::unordered_map indexAccessorIds; + std::unordered_map imageTextureIds; + std::unordered_map meshIds; + + // Each get*Id lambda lazily copies one glTF accessor into ParsedData the + // first time it is referenced, then returns the existing ID on later uses. + const auto getPositionId = [&model, &parsedData, &positionAccessorIds](std::uint32_t accessorIndex) { + // POSITION is required for every drawable primitive, so this helper never returns invalidGltfId. + require(accessorIndex != gltf::invalidGltfId, "glTF primitive is missing POSITION"); + auto [it, inserted] = positionAccessorIds.emplace( + accessorIndex, parsedDataIdFromSize(parsedData.verticesPositions.size(), "POSITION accessor")); + if (inserted) { + parsedData.verticesPositions.push_back(readGltfVec3Accessor(model, accessorIndex, "POSITION")); + } + return it->second; + }; + + const auto getUvId = [&model, &parsedData, &uvAccessorIds](std::uint32_t accessorIndex) { + // Optional accessors use invalidGltfId so the caller can fill defaults later. + if (accessorIndex == gltf::invalidGltfId) { + return gltf::invalidGltfId; + } + auto [it, inserted] = + uvAccessorIds.emplace(accessorIndex, parsedDataIdFromSize(parsedData.uvs.size(), "UV accessor")); + if (inserted) { + parsedData.uvs.push_back(readGltfVec2Accessor(model, accessorIndex, "TEXCOORD_0")); + } + return it->second; + }; + + const auto getNormalId = [&model, &parsedData, &normalAccessorIds](std::uint32_t accessorIndex) { + // Optional accessors use invalidGltfId so the caller can fill defaults later. + if (accessorIndex == gltf::invalidGltfId) { + return gltf::invalidGltfId; + } + auto [it, inserted] = normalAccessorIds.emplace( + accessorIndex, parsedDataIdFromSize(parsedData.normals.size(), "NORMAL accessor")); + if (inserted) { + parsedData.normals.push_back(readGltfVec3Accessor(model, accessorIndex, "NORMAL")); + } + return it->second; + }; + + const auto getTangentId = [&model, &parsedData, &tangentAccessorIds](std::uint32_t accessorIndex) { + // Optional accessors use invalidGltfId so the caller can fill defaults later. + if (accessorIndex == gltf::invalidGltfId) { + return gltf::invalidGltfId; + } + auto [it, inserted] = tangentAccessorIds.emplace( + accessorIndex, parsedDataIdFromSize(parsedData.tangents.size(), "TANGENT accessor")); + if (inserted) { + parsedData.tangents.push_back(readGltfVec4Accessor(model, accessorIndex, "TANGENT")); + } + return it->second; + }; + + const auto getIndexId = [&model, &parsedData, &indexAccessorIds](std::uint32_t accessorIndex) { + // The tutorial renders indexed triangles only, so primitives without indices are rejected. + require(accessorIndex != gltf::invalidGltfId, "glTF primitive must have indices"); + auto [it, inserted] = + indexAccessorIds.emplace(accessorIndex, parsedDataIdFromSize(parsedData.indices.size(), "index accessor")); + if (inserted) { + parsedData.indices.push_back(readGltfIndexAccessor(model, accessorIndex)); + } + return it->second; + }; + + bool foundCamera = parsedData.hasCamera; + const auto handleCamera = [&model, &parsedData, &foundCamera](const tinygltf::Node& node, const glm::mat4& world) { + const std::uint32_t cameraIndex = optionalGltfIndex(node.camera); + if (cameraIndex == gltf::invalidGltfId) { + return; + } + require(!foundCamera, "glTF scene set must contain exactly one camera"); + require(cameraIndex < model.cameras.size(), "glTF camera index is out of range"); + // This tutorial only needs an eye point and a look-at target. + // A camera node's -Z axis is its forward direction in glTF. + parsedData.cameraPos = glm::vec3{world[3]}; + const glm::vec3 forward = glm::normalize(glm::vec3{world * glm::vec4{0.0F, 0.0F, -1.0F, 0.0F}}); + parsedData.cameraLookAt = parsedData.cameraPos + forward; + parsedData.hasCamera = true; + foundCamera = true; + }; + + const auto textureIdForMaterial = [&gltfBaseDir, &model, &parsedData, &imageTextureIds](int materialIndex, + bool normalTexture) { + // Materials may omit a texture. invalidGltfId is the renderer's simple "no texture" sentinel. + const std::uint32_t materialIndexU32 = optionalGltfIndex(materialIndex); + if (materialIndexU32 == gltf::invalidGltfId) { + return gltf::invalidGltfId; + } + require(materialIndexU32 < model.materials.size(), "glTF material index is out of range"); + const tinygltf::Material& material = model.materials[materialIndexU32]; + // This tutorial uses base-color textures for albedo and normalTexture for normal mapping. + const std::uint32_t textureIndex = optionalGltfIndex( + normalTexture ? material.normalTexture.index : material.pbrMetallicRoughness.baseColorTexture.index); + return textureIdFromTextureIndex(model, gltfBaseDir, textureIndex, parsedData, imageTextureIds); + }; + + // glTF scenes are trees. + // Accumulate parent transforms as the traversal walks down the tree so each parsed node ends up in world space. + std::function traverseNode; + traverseNode = [&getIndexId, &getNormalId, &getPositionId, &getTangentId, &getUvId, &handleCamera, &meshIds, &model, + &parsedData, &textureIdForMaterial, + &traverseNode](std::uint32_t nodeIndex, const glm::mat4& parentTransform) { + require(nodeIndex != gltf::invalidGltfId && nodeIndex < model.nodes.size(), + "glTF scene node index is out of range"); + const tinygltf::Node& gltfNode = model.nodes[nodeIndex]; + + // Parent transform first, then this node's local transform, gives the node's world transform. + const glm::mat4 worldTransform = parentTransform * gltfNodeLocalTransform(gltfNode); + + // A camera can be attached to any node, including nodes without meshes. + handleCamera(gltfNode, worldTransform); + + const std::uint32_t gltfMeshIndex = optionalGltfIndex(gltfNode.mesh); + if (gltfMeshIndex != gltf::invalidGltfId) { + // A glTF mesh can contain multiple primitives, each with its own material. + require(gltfMeshIndex < model.meshes.size(), "glTF mesh index is out of range"); + const tinygltf::Mesh& mesh = model.meshes[gltfMeshIndex]; + const std::uint32_t primitiveCount = safeCastToU32(mesh.primitives.size()); + + for (std::uint32_t primitiveIndex = 0; primitiveIndex < primitiveCount; ++primitiveIndex) { + + const tinygltf::Primitive& primitive = mesh.primitives[primitiveIndex]; + + // Each primitive is one indexed triangle stream with one material. + require(primitive.mode == -1 || primitive.mode == TINYGLTF_MODE_TRIANGLES, + "Only glTF triangle primitives are supported"); + + // POSITION is required to draw. UVs, normals, and tangents are optional; + // buildMeshGeometryData fills simple defaults when they are absent. + const auto positionIt = primitive.attributes.find("POSITION"); + require(positionIt != primitive.attributes.end(), "glTF primitive is missing POSITION"); + const auto uvIt = primitive.attributes.find("TEXCOORD_0"); + const auto normalIt = primitive.attributes.find("NORMAL"); + const auto tangentIt = primitive.attributes.find("TANGENT"); + + // Store IDs to shared accessor data. buildMeshGeometryData expands these IDs later. + const Mesh parsedMesh{ + .name = gltfMeshPrimitiveName(mesh, gltfMeshIndex, primitiveIndex, mesh.primitives.size()), + .verticesPositionId = getPositionId(requiredGltfIndex(positionIt->second, "POSITION accessor")), + .indicesId = getIndexId(requiredGltfIndex(primitive.indices, "primitive indices accessor")), + .uvId = uvIt != primitive.attributes.end() ? getUvId(optionalGltfIndex(uvIt->second)) + : gltf::invalidGltfId, + .normalId = normalIt != primitive.attributes.end() + ? getNormalId(optionalGltfIndex(normalIt->second)) + : gltf::invalidGltfId, + .tangentId = tangentIt != primitive.attributes.end() + ? getTangentId(optionalGltfIndex(tangentIt->second)) + : gltf::invalidGltfId, + }; + const GeometryKey meshKey{ + .positionId = parsedMesh.verticesPositionId, + .indexId = parsedMesh.indicesId, + .uvId = parsedMesh.uvId, + .normalId = parsedMesh.normalId, + .tangentId = parsedMesh.tangentId, + }; + auto [meshIt, inserted] = + meshIds.emplace(meshKey, parsedDataIdFromSize(parsedData.meshes.size(), "mesh")); + // Multiple nodes can instance the same primitive. Reusing the Mesh entry keeps geometry shared. + if (inserted) { + // Only store a mesh description once even if multiple nodes + // instantiate the same primitive data. + parsedData.meshes.push_back(parsedMesh); + } + + // A parsed node combines where the mesh is drawn with which textures + // that primitive uses. + Node parsedNode = makeParsedNode(worldTransform); + parsedNode.name = gltfNameOrFallback(gltfNode.name, "Node", nodeIndex); + parsedNode.meshId = meshIt->second; + parsedNode.albedoTextureId = textureIdForMaterial(primitive.material, false); + parsedNode.normalTextureId = textureIdForMaterial(primitive.material, true); + // This sample's textured material path expects an albedo texture and a normal map together. + parsedData.nodes.push_back(parsedNode); + } + } + + for (const int childIndex : gltfNode.children) { + // Children inherit the current node transform through worldTransform. + traverseNode(requiredGltfIndex(childIndex, "child node"), worldTransform); + } + }; + + require(!model.scenes.empty(), "glTF file must contain at least one scene"); + // glTF can mark a default scene. If it does not, scene 0 is the usual fallback. + const std::uint32_t sceneIndex = model.defaultScene >= 0 ? safeCastToU32(model.defaultScene) : std::uint32_t{0}; + require(sceneIndex < model.scenes.size(), "glTF default scene index is out of range"); + for (const int nodeIndex : model.scenes[sceneIndex].nodes) { + // Root nodes start with the identity transform because they have no parent. + traverseNode(requiredGltfIndex(nodeIndex, "root node"), glm::mat4{1.0F}); + } + + require(parsedData.nodes.size() > nodeCountBefore || parsedData.hasCamera != hadCameraBefore, + "glTF scene contains no camera or renderable mesh primitives"); +} + +ParsedData parseGltfFiles(std::span paths) +{ + require(!paths.empty(), "At least one glTF file path is required"); + + ParsedData mergedData{}; + for (const std::filesystem::path& path : paths) { + appendGltfFile(path, mergedData); + } + + require(!mergedData.nodes.empty(), "glTF files contain no renderable mesh primitives"); + require(mergedData.hasCamera, "glTF files must contain exactly one camera"); + return mergedData; +} + +util::MeshGeometryData buildMeshGeometryData(std::uint32_t meshId, const ParsedData& gltfData) +{ + // ParsedData keeps shared accessors by ID. + // This function expands one mesh ID into the interleaved vertex/index arrays that Vulkan buffers will upload. + require(meshId < gltfData.meshes.size(), "glTF node references an invalid mesh id"); + + const Mesh& mesh = gltfData.meshes[meshId]; + + require(mesh.verticesPositionId != gltf::invalidGltfId && + mesh.verticesPositionId < gltfData.verticesPositions.size(), + "glTF node references an invalid POSITION id"); + require(mesh.indicesId != gltf::invalidGltfId && mesh.indicesId < gltfData.indices.size(), + "glTF node references an invalid index id"); + + const std::vector& positions = gltfData.verticesPositions[mesh.verticesPositionId]; + const std::vector& sourceIndices = gltfData.indices[mesh.indicesId]; + + // Optional attribute pointers stay null until the mesh proves that stream exists. + const std::vector* uvs = nullptr; + const std::vector* normals = nullptr; + const std::vector* tangents = nullptr; + + // Missing optional attributes use simple defaults below. When an attribute is + // present, glTF requires one value per POSITION for this tutorial path. + if (mesh.uvId != gltf::invalidGltfId) { + require(mesh.uvId < gltfData.uvs.size(), "glTF node references an invalid UV id"); + uvs = &gltfData.uvs[mesh.uvId]; + require(uvs->size() == positions.size(), "glTF UV count must match POSITION count"); + } + if (mesh.normalId != gltf::invalidGltfId) { + require(mesh.normalId < gltfData.normals.size(), "glTF node references an invalid NORMAL id"); + normals = &gltfData.normals[mesh.normalId]; + require(normals->size() == positions.size(), "glTF NORMAL count must match POSITION count"); + } + if (mesh.tangentId != gltf::invalidGltfId) { + require(mesh.tangentId < gltfData.tangents.size(), "glTF node references an invalid TANGENT id"); + tangents = &gltfData.tangents[mesh.tangentId]; + require(tangents->size() == positions.size(), "glTF TANGENT count must match POSITION count"); + } + + MeshGeometryData geometry{}; + geometry.name = gltfNameOrFallback(mesh.name, "Mesh", meshId); + // Reserve storage because we know exactly how many vertices the POSITION accessor contains. + geometry.vertices.reserve(positions.size()); + + for (std::size_t i = 0; i < positions.size(); ++i) { + // Interleave separate glTF attribute arrays into the PackedVertex layout + // used by the shader's vertex input reflection. + geometry.vertices.push_back(PackedVertex{ + .position = positions[i], + .uv = uvs != nullptr ? (*uvs)[i] : glm::vec2{0.0F}, + .normal = normals != nullptr ? (*normals)[i] : glm::vec3{0.0F, 1.0F, 0.0F}, + .tangent = tangents != nullptr ? (*tangents)[i] : glm::vec4{1.0F, 0.0F, 0.0F, 1.0F}, + }); + } + + geometry.indices.reserve(sourceIndices.size()); + for (const std::uint32_t index : sourceIndices) { + // Validate indices while copying so bad assets fail before Vulkan sees them. + require(index < positions.size(), "glTF index references a vertex outside the POSITION accessor"); + // Indices are already normalized to uint32_t by readGltfIndexAccessor(). + geometry.indices.push_back(index); + } + + if (tangents == nullptr && uvs != nullptr && normals != nullptr) { + // Normal maps need tangents. If the asset did not provide them, generate + // reasonable tangents from triangles, UVs, and normals. + generateMissingTangents(geometry); + } + return geometry; +} + +} // namespace gltf + +namespace math { + +glm::mat4 generateRotation(const glm::vec3& eulerAngles) +{ + // Angles are radians. The order here matches generateModel() and keeps the + // tutorial's camera/object controls predictable: rotate around X, then Y, then Z. + return glm::rotate(glm::mat4{1.0F}, eulerAngles.x, glm::vec3{1.0F, 0.0F, 0.0F}) * + glm::rotate(glm::mat4{1.0F}, eulerAngles.y, glm::vec3{0.0F, 1.0F, 0.0F}) * + glm::rotate(glm::mat4{1.0F}, eulerAngles.z, glm::vec3{0.0F, 0.0F, 1.0F}); +} + +glm::mat4 generateModel(const glm::vec3& pos, const glm::vec3& eulerAngles, const glm::vec3& scale) +{ + // With GLM's column-vector convention, this matrix scales the model first, + // then rotates it, then moves it into world space. + return glm::translate(glm::mat4{1.0F}, pos) * generateRotation(eulerAngles) * glm::scale(glm::mat4{1.0F}, scale); +} + +std::pair calculateYawPitch(const glm::vec3& cameraPos, const glm::vec3& cameraLookAt) +{ + // Convert a look-at target back into yaw/pitch controls. Forward -Z is the + // tutorial camera's zero-yaw direction. + const glm::vec3 forward = glm::normalize(cameraLookAt - cameraPos); + return { + std::atan2(forward.x, -forward.z), + std::asin(std::clamp(forward.y, -1.0F, 1.0F)), + }; +} + +glm::vec3 calculateForward(float pitch, float yaw) +{ + // Rebuild the camera forward vector from yaw around world Y and pitch above + // or below the horizontal plane. + return glm::normalize(glm::vec3{ + std::cos(pitch) * std::sin(yaw), + std::sin(pitch), + -std::cos(pitch) * std::cos(yaw), + }); +} + +glm::vec3 calculateRight(const glm::vec3& forward, const glm::vec3& worldUp) +{ + // For a camera looking down -Z with +Y up, forward x up points to +X. + return glm::normalize(glm::cross(forward, worldUp)); +} + +glm::mat4 calculateViewProjection(float cameraPitch, float cameraYaw, const glm::vec3& cameraPos, float aspectRatio, + float verticalFieldOfView, float nearPlane, float farPlane) +{ + require(aspectRatio > 0.0F, "View-projection aspect ratio must be positive"); + require(verticalFieldOfView > 0.0F, "View-projection vertical field of view must be positive"); + require(verticalFieldOfView < glm::pi(), "View-projection vertical field of view must be less than pi"); + require(nearPlane > 0.0F, "View-projection near plane must be positive"); + require(farPlane > nearPlane, "View-projection far plane must be greater than near plane"); + + const glm::vec3 forward = calculateForward(cameraPitch, cameraYaw); + const glm::mat4 view = glm::lookAt(cameraPos, cameraPos + forward, glm::vec3{0.0F, 1.0F, 0.0F}); + glm::mat4 projection = glm::perspective(verticalFieldOfView, aspectRatio, nearPlane, farPlane); + // GLM's perspective helper follows OpenGL's clip-space Y convention. Vulkan's + // viewport coordinates need the projection Y axis flipped. + projection[1][1] *= -1.0F; + // Shader math uses clipPosition = projection * view * model * localPosition. + return projection * view; +} + +} // namespace math + +const char* vkResultName(VkResult result) +{ + // Vulkan's C API reports success/failure with VkResult integers. Names are easier + // to read in tutorial error messages than raw numeric codes. + switch (result) { + case VK_SUCCESS: + return "VK_SUCCESS"; + case VK_NOT_READY: + return "VK_NOT_READY"; + case VK_TIMEOUT: + return "VK_TIMEOUT"; + case VK_EVENT_SET: + return "VK_EVENT_SET"; + case VK_EVENT_RESET: + return "VK_EVENT_RESET"; + case VK_INCOMPLETE: + return "VK_INCOMPLETE"; + case VK_ERROR_OUT_OF_HOST_MEMORY: + return "VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + return "VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: + return "VK_ERROR_INITIALIZATION_FAILED"; + case VK_ERROR_DEVICE_LOST: + return "VK_ERROR_DEVICE_LOST"; + case VK_ERROR_MEMORY_MAP_FAILED: + return "VK_ERROR_MEMORY_MAP_FAILED"; + case VK_ERROR_LAYER_NOT_PRESENT: + return "VK_ERROR_LAYER_NOT_PRESENT"; + case VK_ERROR_EXTENSION_NOT_PRESENT: + return "VK_ERROR_EXTENSION_NOT_PRESENT"; + case VK_ERROR_FEATURE_NOT_PRESENT: + return "VK_ERROR_FEATURE_NOT_PRESENT"; + case VK_ERROR_INCOMPATIBLE_DRIVER: + return "VK_ERROR_INCOMPATIBLE_DRIVER"; + case VK_ERROR_TOO_MANY_OBJECTS: + return "VK_ERROR_TOO_MANY_OBJECTS"; + case VK_ERROR_FORMAT_NOT_SUPPORTED: + return "VK_ERROR_FORMAT_NOT_SUPPORTED"; + case VK_ERROR_FRAGMENTED_POOL: + return "VK_ERROR_FRAGMENTED_POOL"; + case VK_ERROR_UNKNOWN: + return "VK_ERROR_UNKNOWN"; + default: + return "VK_RESULT_UNRECOGNIZED"; + } +} + +void checkVk(VkResult result, std::string_view operation) +{ + // GLFW and a few Vulkan C APIs return VkResult directly instead of throwing exceptions. + if (result != VK_SUCCESS) { + throw std::runtime_error(std::format("{} failed with {}", operation, vkResultName(result))); + } +} + +void require(bool condition, std::string_view message) +{ + // Prefer explicit checks over undefined behavior when tutorial assumptions are violated. + if (!condition) { + throw std::runtime_error(std::string(message)); + } +} + +std::uint32_t readFrameLimitCLI(int argc, char** argv) +{ + // The optional frame limit is used by automated runs/screenshots. Returning + // 0 means "run normally with no fixed frame count". + constexpr std::string_view frameLimitArgument = "--frame-limit"; + + const auto parseFrameLimit = [](std::string_view value) { + if (value.empty()) { + throw std::runtime_error("Frame limit must not be empty"); + } + + std::uint64_t frameLimit = 0; + const char* const begin = value.data(); + const char* const end = begin + value.size(); + // from_chars is locale-independent and reports where parsing stopped, so + // values like "12abc" are rejected instead of partially accepted. + const auto [parsedEnd, error] = std::from_chars(begin, end, frameLimit); + if (error != std::errc{} || parsedEnd != end) { + throw std::runtime_error("Frame limit must be a non-negative integer"); + } + return safeCastToU32(frameLimit); + }; + + if (argc == 1) { + return 0; + } + + if (argc == 3 && std::string_view{argv[1]} == frameLimitArgument) { + // Support --frame-limit 120. + return parseFrameLimit(argv[2]); + } + + throw std::runtime_error(std::format("Usage: {} [--frame-limit N]", argv[0])); +} + +} // namespace util +} // namespace siggraph diff --git a/attachments/siggraph2026_vk_tutorial/src/util.h b/attachments/siggraph2026_vk_tutorial/src/util.h new file mode 100644 index 00000000..76827995 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/src/util.h @@ -0,0 +1,357 @@ +/* Copyright (c) 2026, Khronos Group and contributors + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace siggraph { +namespace util { + +// Helper functions for logging. + +// Small optimization to avoid calling std::format for messages without arguments. +inline void log_msg(std::format_string<> msg) { std::cout << msg.get() << '\n'; } + +template inline void log_msg(std::format_string msg, T&&... args) +{ + // Format the message and write one complete log line. + std::cout << std::format(msg, std::forward(args)...) << '\n'; +} + +// Utility class to handle Vulkan binary buffers. +// Notice we have an offset since shader objects creation needs aligned buffers. +struct BinaryBuffer { + std::vector storage; + std::size_t offset = 0; + std::size_t size = 0; + + BinaryBuffer(const BinaryBuffer&) = delete; + BinaryBuffer(BinaryBuffer&&) = default; + + BinaryBuffer& operator=(const BinaryBuffer&) = delete; + BinaryBuffer& operator=(BinaryBuffer&&) = delete; + + BinaryBuffer(std::size_t size, std::size_t alignment); + + // Return the aligned start of the useful byte range. + [[nodiscard]] const std::byte* data() const { return storage.data() + offset; } + + [[nodiscard]] std::byte* data() { return storage.data() + offset; } + + // Most Vulkan-Hpp APIs accept spans, so expose the useful range directly. + [[nodiscard]] std::span as_byte_span() const { return {data(), size}; } +}; + +// CPU-side image data used before main.cpp uploads a texture to Vulkan. +struct ImageRgba8 { + std::uint32_t m_width = 0; + std::uint32_t m_height = 0; + std::vector m_pixels; +}; + +// Interleaved vertex layout used by the tutorial shaders. +struct PackedVertex { + glm::vec3 position{}; + glm::vec2 uv{}; + glm::vec3 normal{}; + glm::vec4 tangent{1.0F, 0.0F, 0.0F, 1.0F}; +}; + +// One renderable mesh after glTF accessors have been expanded for Vulkan upload. +struct MeshGeometryData { + std::string name; + std::vector vertices; + std::vector indices; +}; + +// Read a compiled SPIR-V binary and validate that it is aligned as 32-bit SPIR-V words. +[[nodiscard]] BinaryBuffer readSpirvFile(const std::filesystem::path& path); + +// Read/write raw binary blobs. Missing or empty input files return nullopt so optional caches can fall back cleanly. +// Note: alignment = 0 means there are no alignment requirements. +[[nodiscard]] std::optional readBinaryFile(const std::filesystem::path& path, std::size_t alignment); +void writeBinaryFile(const std::filesystem::path& path, std::span data); + +// Convert an 8-bit sRGB display color into linear RGB values for shader/render-target math. +[[nodiscard]] glm::vec3 sRgbToLinear(std::uint8_t r, std::uint8_t g, std::uint8_t b); + +[[nodiscard]] std::uint64_t combineHash(std::span data, std::uint64_t seed = 0); +[[nodiscard]] std::uint64_t combineHash(std::span data, std::uint64_t seed = 0); +[[nodiscard]] std::uint64_t combineHash(std::string_view value, std::uint64_t seed = 0); + +// Hash integral and enum values by expanding them into stable little-endian bytes. +template [[nodiscard]] inline std::uint64_t combineHash(T value, std::uint64_t seed = 0) +{ + if constexpr (std::is_enum_v) { + return combineHash(static_cast>(value), seed); + } + else { + static_assert(std::is_integral_v, "combineHash supports integral values, enum values, strings, and bytes"); + using Unsigned = std::make_unsigned_t; + const Unsigned normalized = static_cast(value); + std::array bytes{}; + for (std::size_t i = 0; i < bytes.size(); ++i) { + bytes[i] = static_cast((normalized >> (i * 8U)) & static_cast(0xFFU)); + } + return combineHash(std::span{bytes.data(), bytes.size()}, seed); + } +} +// Three or more values are hashed left-to-right. Two arguments keep the existing value+seed meaning. +template +[[nodiscard]] inline std::uint64_t combineHash(First first, Second second, Third third, Rest... rest) +{ + std::uint64_t seed = combineHash(first); + seed = combineHash(second, seed); + seed = combineHash(third, seed); + ((seed = combineHash(rest, seed)), ...); + return seed; +} + +// Read an image from disk and convert it to tightly packed RGBA8 pixels. +// The pixels are stored as uint32_t words only to make Vulkan upload sizing simple; +// in memory the byte order is still R, G, B, A for VK_FORMAT_R8G8B8A8_UNORM. +[[nodiscard]] ImageRgba8 readImageFileRgba8(const std::filesystem::path& path); + +// Convert common VkResult values into readable names for exception messages. +[[nodiscard]] const char* vkResultName(VkResult result); + +// Throw when a Vulkan C API call fails. This keeps setup code readable in the tutorial. +void checkVk(VkResult result, std::string_view operation); + +// General assertion helper for tutorial invariants that should fail loudly. +void require(bool condition, std::string_view message); + +// Read the optional --frame-limit CLI value. Returns 0 when no limit is requested. +[[nodiscard]] std::uint32_t readFrameLimitCLI(int argc, char** argv); + +// Cast integral values only after validating that the destination type can represent the value. +template [[nodiscard]] inline T safeCastTo(V value) +{ + static_assert(std::is_integral_v && std::is_integral_v, "safeCastTo supports integral values"); + require(std::in_range(value), "Integer cast is out of range"); + return static_cast(value); +} + +template [[nodiscard]] inline std::uint32_t safeCastToU32(V value) +{ + return safeCastTo(value); +} + +// Vulkan dispatchable handles are pointers; non-dispatchable handles are integer-like. +// Debug object naming wants either form packed into one uint64_t value. +template [[nodiscard]] inline static std::uint64_t rawHandleToUint64(RawHandle handle) +{ + if constexpr (std::is_pointer_v) { + return reinterpret_cast(handle); + } + else { + return static_cast(handle); + } +} + +// Round value up to the next multiple of alignment. Vulkan descriptor heap layouts +// use this for byte offsets where the alignment value comes from device properties. +template [[nodiscard]] inline static constexpr T alignUp(T value, T alignment) +{ + if (alignment == 0) { + return value; + } + return (value + alignment - 1) / alignment * alignment; +} + +// Return the byte/count offset needed to move value to the next aligned value. +// This is useful when Vulkan returns an unaligned base address but later requires +// binding an aligned subrange inside the same allocation. +template [[nodiscard]] inline static S alignedOffset(T value, S alignment) +{ + if (alignment == 0) { + return 0; + } + + const T alignedValue = alignUp(value, safeCastTo(alignment)); + return safeCastTo(alignedValue - value); +} + +// Return a size large enough to contain an aligned subrange of rangeSize bytes. +// The extra alignment - 1 bytes cover the worst case where the base address is +// one byte past an aligned address. +template [[nodiscard]] inline static constexpr T alignedAllocationSize(T rangeSize, T alignment) +{ + return rangeSize + ((alignment > 0) ? (alignment - 1) : 0); +} + +} // namespace util + +namespace util::math { + +// Build a rotation matrix from tutorial Euler-angle controls. +[[nodiscard]] glm::mat4 generateRotation(const glm::vec3& eulerAngles); + +// Build a full model matrix from position, rotation, and scale. +[[nodiscard]] glm::mat4 generateModel(const glm::vec3& pos, const glm::vec3& eulerAngles, const glm::vec3& scale); + +// Convert a camera position and look-at point into yaw/pitch controls. +[[nodiscard]] std::pair calculateYawPitch(const glm::vec3& cameraPos, const glm::vec3& cameraLookAt); + +// Convert yaw/pitch controls back into a normalized forward direction. +[[nodiscard]] glm::vec3 calculateForward(float pitch, float yaw); + +// Calculate the camera right vector from forward and world-up directions. +[[nodiscard]] glm::vec3 calculateRight(const glm::vec3& forward, + const glm::vec3& worldUp = glm::vec3{0.0F, 1.0F, 0.0F}); + +// Build the projection * view matrix used by the vertex shader. +[[nodiscard]] glm::mat4 calculateViewProjection(float cameraPitch, float cameraYaw, const glm::vec3& cameraPos, + float aspectRatio, float verticalFieldOfView = 0.78539816339F, + float nearPlane = 0.1F, float farPlane = 100.0F); + +} // namespace util::math + +namespace util::slang { + +// Descriptor binding information reflected from one Slang shader parameter. +struct ShaderResourceBinding { + std::uint32_t set = 0; + std::uint32_t binding = 0; + VkSpirvResourceTypeFlagsEXT resourceMask = 0; +}; + +// Vertex shader input location and the Vulkan format that describes its data. +struct VertexInput { + std::uint32_t location = 0; + vk::Format format = vk::Format::eUndefined; +}; + +// Dynamic vertex-input state needed before drawing PackedVertex buffers. +struct PackedVertexInputLayout { + std::vector bindings; + std::vector attributes; +}; + +// Read Slang reflection JSON and return descriptor-backed shader resources by name. +// CMake asks slangc to emit this JSON next to the compiled SPIR-V. +[[nodiscard]] std::unordered_map +calculateReflectionShaderResourceBindings(const std::filesystem::path& path); + +// Merge descriptor-backed shader resources across multiple reflection files. +// A shared resource name must resolve to the same set, binding, and resource mask in every stage. +[[nodiscard]] std::unordered_map +collectShaderResourceBindings(std::span reflectionPaths); + +// Read Slang reflection JSON and return vertex inputs by field name. +// CMake asks slangc to emit this JSON next to the compiled SPIR-V. +[[nodiscard]] std::unordered_map +calculateReflectionVertexInputs(const std::filesystem::path& path); + +// Build Vulkan vertex binding/attribute descriptions from one reflection file. +[[nodiscard]] PackedVertexInputLayout calculatePackedVertexInputLayout(const std::filesystem::path& reflectionPath); + +} // namespace util::slang + +namespace util::gltf { + +// Sentinel ID for optional glTF data that is not present. +inline constexpr std::uint32_t invalidGltfId = std::numeric_limits::max(); + +// A parsed glTF mesh references shared accessor data by compact IDs. +struct Mesh { + std::string name; + // Required attribute/index streams. + std::uint32_t verticesPositionId = invalidGltfId; + std::uint32_t indicesId = invalidGltfId; + // Optional attribute streams. Missing ones use invalidGltfId. + std::uint32_t uvId = invalidGltfId; + std::uint32_t normalId = invalidGltfId; + std::uint32_t tangentId = invalidGltfId; +}; + +// One scene instance: transform, mesh ID, and the textures used by that draw. +struct Node { + std::string name; + // The parser stores transform components instead of a matrix so main.cpp can + // build model matrices in the same way as the rest of the tutorial. + glm::vec3 pos{}; + glm::vec3 eulerAngles{}; + glm::vec3 scale{1.0F}; + // References one Mesh entry in ParsedData::meshes. + std::uint32_t meshId = invalidGltfId; + // Texture IDs index ParsedData::textures, or invalidGltfId when absent. + std::uint32_t albedoTextureId = invalidGltfId; + std::uint32_t normalTextureId = invalidGltfId; +}; + +// Texture metadata from the glTF file. Image decoding happens later. +struct GltfTexture { + std::string name; + // Resolved filesystem path to the external texture image. + std::string filename; +}; + +// Parsed scene data kept in simple arrays so main.cpp can upload it step by step. +struct ParsedData { + // Camera values come from the single camera in the parsed glTF scene set. + glm::vec3 cameraPos{}; + glm::vec3 cameraLookAt{}; + bool hasCamera = false; + // Nodes are draw instances. Meshes are unique geometry descriptions. + std::vector nodes; + std::vector meshes; + // Each vector below stores one copied glTF accessor stream. + std::vector> verticesPositions; + std::vector> indices; + std::vector> uvs; + std::vector> normals; + std::vector> tangents; + std::vector textures; +}; + +// Parse a basic glTF 2.0 scene into CPU arrays and shared geometry IDs. +// Texture output is metadata only; image decoding stays with util image readers. +void appendGltfFile(const std::filesystem::path& path, ParsedData& parsedData); +[[nodiscard]] ParsedData parseGltfFiles(std::span paths); + +// Expand one parsed mesh into the packed vertex/index arrays expected by Vulkan. +[[nodiscard]] util::MeshGeometryData buildMeshGeometryData(std::uint32_t meshId, const ParsedData& gltfData); + +} // namespace util::gltf +} // namespace siggraph diff --git a/en/courses/siggraph2026_vk_tutorial/00_Overview.adoc b/en/courses/siggraph2026_vk_tutorial/00_Overview.adoc index ea3f6baf..f71e6b0f 100644 --- a/en/courses/siggraph2026_vk_tutorial/00_Overview.adoc +++ b/en/courses/siggraph2026_vk_tutorial/00_Overview.adoc @@ -4,8 +4,8 @@ Welcome! In this series, we will explain how to write a Vulkan renderer using new Vulkan extensions and the recommended best practices from Khronos as of 2026. This tutorial uses shader objects, dynamic rendering, and descriptor heap techniques, and is based on our SIGGRAPH 2026 course "How to write a Vulkan application in 2026". -Slides available: Coming soon. -Source code available: Coming soon. +Slides: Coming soon. +Source code: link:https://github.com/KhronosGroup/Vulkan-Tutorial/blob/main/attachments/siggraph2026_vk_tutorial[attachments/siggraph2026_vk_tutorial]. By the end, you'll learn how to: @@ -23,7 +23,7 @@ SIGGRAPH page: Session details (from the SIGGRAPH listing): - Course Title: How to write a Vulkan application in 2026 -- Course Date and Time: Wed Jul 22, 9am-12:15pm PDT +- Course Date and Time: Wednesday, July 22, 2026, 9:00 AM-12:15 PM PDT - Course Location: Petree D - Interest Areas: Gaming & Interactive - Keywords: Education, Games, Real-Time, Rendering From dc42c16ee30fbfb3b64c3bfb034554b181c0cb19 Mon Sep 17 00:00:00 2001 From: Iago Calvo Lista Date: Thu, 9 Jul 2026 19:09:43 +0100 Subject: [PATCH 2/3] Fix licenses --- LICENSES/MIT.txt | 9 ++++++++ .../siggraph2026_vk_tutorial/CMakeLists.txt | 22 +++++++++++++++++++ attachments/siggraph2026_vk_tutorial/LICENSE | 2 +- .../init_submodules.py | 22 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 LICENSES/MIT.txt diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 00000000..3f28a5f5 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/attachments/siggraph2026_vk_tutorial/CMakeLists.txt b/attachments/siggraph2026_vk_tutorial/CMakeLists.txt index 7b531a41..80f4587d 100644 --- a/attachments/siggraph2026_vk_tutorial/CMakeLists.txt +++ b/attachments/siggraph2026_vk_tutorial/CMakeLists.txt @@ -1,3 +1,25 @@ +# Copyright (c) 2026, Khronos Group and contributors +# +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + cmake_minimum_required(VERSION 3.26) # This tutorial builds one executable that renders a descriptor-heap glTF scene with Vulkan shader objects. diff --git a/attachments/siggraph2026_vk_tutorial/LICENSE b/attachments/siggraph2026_vk_tutorial/LICENSE index 614c19e3..5d8649ae 100644 --- a/attachments/siggraph2026_vk_tutorial/LICENSE +++ b/attachments/siggraph2026_vk_tutorial/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Khronos Group +Copyright (c) 2026 Khronos Group and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/attachments/siggraph2026_vk_tutorial/init_submodules.py b/attachments/siggraph2026_vk_tutorial/init_submodules.py index 03c39210..7ee23a39 100644 --- a/attachments/siggraph2026_vk_tutorial/init_submodules.py +++ b/attachments/siggraph2026_vk_tutorial/init_submodules.py @@ -1,3 +1,25 @@ +# Copyright (c) 2026, Khronos Group and contributors +# +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + from pathlib import Path import subprocess import sys From 1d8c3a6cef4453769a4ec2c3664d1559dea225c6 Mon Sep 17 00:00:00 2001 From: Iago Calvo Lista Date: Fri, 10 Jul 2026 19:01:05 +0100 Subject: [PATCH 3/3] PR fixes Change Cmake to clone the modules instead of using submodules. Remove HLSL annotations from Slang. Readme is AsciiDoc. Clarify comment about unused vertex output Improve Cmake --- .gitignore | 6 +- .../siggraph2026_vk_tutorial/.gitignore | 2 + .../siggraph2026_vk_tutorial/.gitmodules | 29 -- .../siggraph2026_vk_tutorial/CMakeLists.txt | 312 ++++++++++++------ .../CMakePresets.json | 31 +- .../siggraph2026_vk_tutorial/README.adoc | 115 +++++++ .../siggraph2026_vk_tutorial/README.md | 135 -------- .../cmake/fetch_git.cmake | 80 +++++ .../init_submodules.py | 192 ----------- .../shaders/albedo.frag.slang | 27 +- .../shaders/basic.frag.slang | 13 +- .../shaders/basic.vert.slang | 18 +- .../shaders/solid_color.frag.slang | 29 +- 13 files changed, 471 insertions(+), 518 deletions(-) delete mode 100644 attachments/siggraph2026_vk_tutorial/.gitmodules create mode 100644 attachments/siggraph2026_vk_tutorial/README.adoc delete mode 100644 attachments/siggraph2026_vk_tutorial/README.md create mode 100644 attachments/siggraph2026_vk_tutorial/cmake/fetch_git.cmake delete mode 100644 attachments/siggraph2026_vk_tutorial/init_submodules.py diff --git a/.gitignore b/.gitignore index 6feb509a..3bdf1b62 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,4 @@ attachments/simple_engine/android/gradle/wrapper/** attachments/simple_engine/android/gradlew attachments/simple_engine/android/gradlew.bat -attachments/template/build/** - -attachments/siggraph2026_vk_tutorial/.git/ -attachments/siggraph2026_vk_tutorial/external/ -attachments/siggraph2026_vk_tutorial/data/external +attachments/template/build/** \ No newline at end of file diff --git a/attachments/siggraph2026_vk_tutorial/.gitignore b/attachments/siggraph2026_vk_tutorial/.gitignore index 30f25233..060e18c7 100644 --- a/attachments/siggraph2026_vk_tutorial/.gitignore +++ b/attachments/siggraph2026_vk_tutorial/.gitignore @@ -4,6 +4,8 @@ out/ .cache/ .vscode/ __pycache__/ +external/ +data/external/ CMakeUserPresets.json compile_commands.json imgui.ini diff --git a/attachments/siggraph2026_vk_tutorial/.gitmodules b/attachments/siggraph2026_vk_tutorial/.gitmodules deleted file mode 100644 index c086db7d..00000000 --- a/attachments/siggraph2026_vk_tutorial/.gitmodules +++ /dev/null @@ -1,29 +0,0 @@ -[submodule "glfw"] - path = external/glfw - url = https://github.com/glfw/glfw.git - commit = b00e6a8a88ad1b60c0a045e696301deb92c9a13e -[submodule "glm"] - path = external/glm - url = https://github.com/g-truc/glm.git - commit = 6f14f4792a0cde5d0cf2c910506724d61cb95834 -[submodule "vk-bootstrap"] - path = external/vk-bootstrap - url = https://github.com/charles-lunarg/vk-bootstrap.git - commit = 5ca6780498864ae4c12f3a594ee6a6c5133d4ce0 -[submodule "nlohmann"] - path = external/nlohmann - url = https://github.com/nlohmann/json.git - commit = 4fad4468974a7b1b26d374b1c5955d2ac7d449b0 -[submodule "stb"] - path = external/stb - url = https://github.com/nothings/stb.git - commit = 31c1ad37456438565541f4919958214b6e762fb4 -[submodule "tinygltf"] - path = external/tinygltf - url = https://github.com/syoyo/tinygltf.git - commit = d31c16e333a6c8d593cad43f325f4e1825dd4776 -[submodule "gltf-sample-assets"] - path = data/external/sponza - url = https://github.com/KhronosGroup/glTF-Sample-Assets.git - commit = 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf - sparse-path = Models/Sponza diff --git a/attachments/siggraph2026_vk_tutorial/CMakeLists.txt b/attachments/siggraph2026_vk_tutorial/CMakeLists.txt index 80f4587d..88cdda9d 100644 --- a/attachments/siggraph2026_vk_tutorial/CMakeLists.txt +++ b/attachments/siggraph2026_vk_tutorial/CMakeLists.txt @@ -22,72 +22,181 @@ cmake_minimum_required(VERSION 3.26) -# This tutorial builds one executable that renders a descriptor-heap glTF scene with Vulkan shader objects. +# This tutorial builds one executable that renders a glTF scene. +# The main objective is to showcase the use of Vulkan. project(vulkan_siggraph LANGUAGES CXX) -# Export compile_commands.json so editors and language servers can understand the C++ build. +# Export compile_commands.json. +# This allows editors and language servers to understand the C++ build. set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL "Export compile_commands.json for language servers") -# The Vulkan SDK provides headers, the loader library, and the Slang compiler used below. +# Vulkan provides headers and the loader library. find_package(Vulkan REQUIRED) +# Git is used to populate pinned dependencies. +find_package(Git REQUIRED) -# Keep third-party libraries small: this sample only needs their runtime/library targets. +# Configure third party libraries set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE) set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(GLFW_INSTALL OFF CACHE BOOL "" FORCE) + +set(GLM_BUILD_LIBRARY OFF CACHE BOOL "" FORCE) set(GLM_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GLM_BUILD_INSTALL OFF CACHE BOOL "" FORCE) + +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +set(JSON_Install OFF CACHE BOOL "" FORCE) +set(JSON_SystemInclude ON CACHE BOOL "" FORCE) + set(VK_BOOTSTRAP_TEST OFF CACHE BOOL "" FORCE) set(VK_BOOTSTRAP_WERROR OFF CACHE BOOL "" FORCE) +set(VK_BOOTSTRAP_INSTALL OFF CACHE BOOL "" FORCE) + +set(TINYGLTF_BUILD_LOADER_EXAMPLE OFF CACHE BOOL "" FORCE) +set(TINYGLTF_BUILD_GL_EXAMPLES OFF CACHE BOOL "" FORCE) +set(TINYGLTF_BUILD_VALIDATOR_EXAMPLE OFF CACHE BOOL "" FORCE) +set(TINYGLTF_BUILD_BUILDER_EXAMPLE OFF CACHE BOOL "" FORCE) +set(TINYGLTF_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(TINYGLTF_HEADER_ONLY ON CACHE BOOL "" FORCE) +set(TINYGLTF_INSTALL OFF CACHE BOOL "" FORCE) + +set(SIGGRAPH_EXTERNAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external") +set(SIGGRAPH_DATA_EXTERNAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data/external") +set(SIGGRAPH_FETCH_GIT_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/cmake/fetch_git.cmake") + +# Ensure one pinned Git checkout exists in the source tree. +function(siggraph_checkout_git name repository tag source_dir sparse_path) + execute_process( + COMMAND + "${CMAKE_COMMAND}" + "-DGIT_EXECUTABLE=${GIT_EXECUTABLE}" + "-DGIT_REPOSITORY=${repository}" + "-DGIT_TAG=${tag}" + "-DSOURCE_DIR=${source_dir}" + "-DSPARSE_PATH=${sparse_path}" -P + "${SIGGRAPH_FETCH_GIT_SCRIPT}" + COMMAND_ERROR_IS_FATAL ANY) + + set("${name}_SOURCE_DIR" "${source_dir}" PARENT_SCOPE) +endfunction() -# Add vendored dependencies. vk-bootstrap removes a lot of boilerplate from instance, -# device, queue, and swapchain creation, which keeps the tutorial focused on Vulkan flow. -add_subdirectory(external/glfw) -add_subdirectory(external/glm) -add_subdirectory(external/vk-bootstrap) - -# Slang compiles the small tutorial shaders to SPIR-V, the binary shader format Vulkan consumes. -find_program(SLANGC_EXECUTABLE - NAMES slangc - HINTS "$ENV{VULKAN_SDK}/Bin" - DOC "Path to the Slang compiler executable" +# Dependencies are pinned here. The last argument is normally is used for sparse +# checkouts. +siggraph_checkout_git( + glfw + https://github.com/glfw/glfw.git + b00e6a8a88ad1b60c0a045e696301deb92c9a13e + "${SIGGRAPH_EXTERNAL_DIR}/glfw" + "") + +siggraph_checkout_git( + glm + https://github.com/g-truc/glm.git + 6f14f4792a0cde5d0cf2c910506724d61cb95834 + "${SIGGRAPH_EXTERNAL_DIR}/glm" + "") + +siggraph_checkout_git( + vk_bootstrap + https://github.com/charles-lunarg/vk-bootstrap.git + 5ca6780498864ae4c12f3a594ee6a6c5133d4ce0 + "${SIGGRAPH_EXTERNAL_DIR}/vk-bootstrap" + "") + +siggraph_checkout_git( + nlohmann_json + https://github.com/nlohmann/json.git + 4fad4468974a7b1b26d374b1c5955d2ac7d449b0 + "${SIGGRAPH_EXTERNAL_DIR}/nlohmann" + "") + +siggraph_checkout_git( + stb + https://github.com/nothings/stb.git + 31c1ad37456438565541f4919958214b6e762fb4 + "${SIGGRAPH_EXTERNAL_DIR}/stb" + "") + +siggraph_checkout_git( + tinygltf + https://github.com/syoyo/tinygltf.git + d31c16e333a6c8d593cad43f325f4e1825dd4776 + "${SIGGRAPH_EXTERNAL_DIR}/tinygltf" + "") + +siggraph_checkout_git( + gltf_sample_assets + https://github.com/KhronosGroup/glTF-Sample-Assets.git + 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf + "${SIGGRAPH_DATA_EXTERNAL_DIR}/sponza" + Models/Sponza # Sparse checkout. Only checkouts Sponza ) + +# Add CMake-aware dependencies from the pinned checkouts. +add_subdirectory("${glfw_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/deps/glfw") +add_subdirectory("${glm_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/deps/glm") +add_subdirectory("${vk_bootstrap_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/deps/vk-bootstrap") +add_subdirectory("${nlohmann_json_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/deps/nlohmann") +add_subdirectory("${tinygltf_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/deps/tinygltf") + +# stb has no CMake project, expose its fetched include directory as an interface +# target for normal target_link_libraries usage. +add_library(stb INTERFACE) +add_library(stb::stb ALIAS stb) +target_include_directories(stb SYSTEM INTERFACE "${stb_SOURCE_DIR}") + +# tinygltf provides a target in header - only mode, but not a namespaced alias. +if(NOT TARGET tinygltf::tinygltf) + add_library(tinygltf::tinygltf ALIAS tinygltf) +endif() +target_include_directories(tinygltf SYSTEM INTERFACE "${tinygltf_SOURCE_DIR}") + +# Slang compiles the small tutorial shaders to SPIR - V. +find_program( + SLANGC_EXECUTABLE + NAMES slangc + HINTS "$ENV{VULKAN_SDK}/Bin" + DOC "Path to the Slang compiler executable") if(NOT SLANGC_EXECUTABLE) - message(FATAL_ERROR "slangc was not found. Configure with -DSLANGC_EXECUTABLE= or add slangc to PATH.") + message( + FATAL_ERROR + "slangc was not found. Configure with -DSLANGC_EXECUTABLE= or add slangc to PATH." + ) endif() # CMake writes compiled shader outputs into the build tree, not the source tree. set(SHADER_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders") -# Helper for declaring one shader compile step. The executable depends on the custom -# targets created here, so shaders are compiled before the app is linked/run. +# Helper for declaring one shader compile step. The executable depends on the +# custom targets created here, so shaders are compiled before the app is linked. function(add_slang_shader target_name source_file entry_point stage output_file) set(reflection_json "${output_file}.json") + get_filename_component(output_dir "${output_file}" DIRECTORY) - # Store shader source files so they can be added to the project + # Store shader source files so they can be added to the project. set_source_files_properties("${source_file}" PROPERTIES HEADER_FILE_ONLY TRUE) set_property(GLOBAL APPEND PROPERTY SIGGRAPH_SHADER_SOURCES "${source_file}") add_custom_command( - OUTPUT "${output_file}" "${reflection_json}" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${SHADER_OUTPUT_DIR}" - COMMAND "${SLANGC_EXECUTABLE}" - "${source_file}" - -entry "${entry_point}" # Main function, for example vertexMain or fragmentMain. - -stage "${stage}" # vertex, fragment, etc. - -target spirv - -profile spirv_1_5 - -matrix-layout-column-major # Keep Slang's matrix layout explicit. - -reflection-json "${reflection_json}" # Generate reflection data as JSON. - -o "${output_file}" - DEPENDS "${source_file}" - VERBATIM - COMMENT "Compiling ${source_file}" - ) - add_custom_target("${target_name}" - DEPENDS "${output_file}" "${reflection_json}" - SOURCES "${source_file}" - ) + OUTPUT "${output_file}" "${reflection_json}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${output_dir}" + COMMAND + "${SLANGC_EXECUTABLE}" "${source_file}" -entry + "${entry_point}" # Main function, for example vertexMain or fragmentMain. + -stage "${stage}" # vertex, fragment, etc. + -target spirv -profile spirv_1_5 + # Slang uses row major layout by default, but we want to use column major. + -matrix-layout-column-major -reflection-json + "${reflection_json}" # Generate reflection data as JSON. + -o "${output_file}" + DEPENDS "${source_file}" + VERBATIM USES_TERMINAL + COMMENT "Compiling ${source_file}") + add_custom_target( + "${target_name}" + DEPENDS "${output_file}" "${reflection_json}" + SOURCES "${source_file}") endfunction() # Output filenames used by the C++ code through SIGGRAPH_SHADER_DIR. @@ -96,97 +205,96 @@ set(BASIC_FRAG_SPV "${SHADER_OUTPUT_DIR}/basic.frag.spv") set(ALBEDO_FRAG_SPV "${SHADER_OUTPUT_DIR}/albedo.frag.spv") set(SOLID_COLOR_FRAG_SPV "${SHADER_OUTPUT_DIR}/solid_color.frag.spv") -# Compile the vertex shader. It transforms scene vertices and reads per-object transforms through descriptor heap mappings. +# Compile the vertex shader. It transforms scene vertices and reads per object +# transforms through descriptor heap mappings. add_slang_shader( - basic_vertex_shader - "${CMAKE_CURRENT_SOURCE_DIR}/shaders/basic.vert.slang" - vertexMain - vertex - "${BASIC_VERT_SPV}" -) + basic_vertex_shader "${CMAKE_CURRENT_SOURCE_DIR}/shaders/basic.vert.slang" + vertexMain + vertex + "${BASIC_VERT_SPV}") -# Compile the fragment shader. It samples textures selected through a push-index heap mapping. +# Compile the fragment shader. It samples textures selected through a push - +# index heap mapping. add_slang_shader( - basic_fragment_shader - "${CMAKE_CURRENT_SOURCE_DIR}/shaders/basic.frag.slang" - fragmentMain - fragment - "${BASIC_FRAG_SPV}" -) + basic_fragment_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/basic.frag.slang" + fragmentMain + fragment + "${BASIC_FRAG_SPV}") -# Compile the albedo-only fragment shader. It samples albedo and lights with interpolated geometry normals. +# Compile the albedo - only fragment shader.It samples albedo but not the normal texture. add_slang_shader( - albedo_fragment_shader - "${CMAKE_CURRENT_SOURCE_DIR}/shaders/albedo.frag.slang" - fragmentMain - fragment - "${ALBEDO_FRAG_SPV}" -) + albedo_fragment_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/albedo.frag.slang" + fragmentMain + fragment + "${ALBEDO_FRAG_SPV}") -# Compile the solid-color fragment shader. It reads a constant-offset uniform buffer descriptor. +# Compile the solid - color fragment shader.It reads a constant - offset uniform +# buffer descriptor. add_slang_shader( - solid_color_fragment_shader - "${CMAKE_CURRENT_SOURCE_DIR}/shaders/solid_color.frag.slang" - fragmentMain - fragment - "${SOLID_COLOR_FRAG_SPV}" -) + solid_color_fragment_shader + "${CMAKE_CURRENT_SOURCE_DIR}/shaders/solid_color.frag.slang" + fragmentMain + fragment + "${SOLID_COLOR_FRAG_SPV}") # Main tutorial executable sources. -add_executable(vulkan_siggraph - src/main.cpp - src/main.h - src/util.cpp - src/util.h -) +add_executable(vulkan_siggraph src/main.cpp src/main.h src/util.cpp src/util.h) -# Save shaders in their own source group called shaders -# Visual Studio will display this as a folder: vulkan_siggraph/shaders +# Save shaders in their own source group called Shaders. get_property(SIGGRAPH_SHADER_SOURCES GLOBAL PROPERTY SIGGRAPH_SHADER_SOURCES) target_sources(vulkan_siggraph PRIVATE ${SIGGRAPH_SHADER_SOURCES}) source_group("Shaders" FILES ${SIGGRAPH_SHADER_SOURCES}) # Make shader compilation part of the normal application build. -add_dependencies(vulkan_siggraph basic_vertex_shader basic_fragment_shader albedo_fragment_shader solid_color_fragment_shader) - -# SIGGRAPH_SHADER_DIR tells the app where CMake put the SPIR-V files. -# SIGGRAPH_DATA_DIR points at the sample assets used by the descriptor heap tutorial. -# The Vulkan-Hpp definitions enable dynamic dispatch loading and designated initializers. -target_compile_definitions(vulkan_siggraph - PRIVATE - GLFW_INCLUDE_NONE - SIGGRAPH_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" - SIGGRAPH_SHADER_DIR="${SHADER_OUTPUT_DIR}" - VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 - VULKAN_HPP_NO_STRUCT_CONSTRUCTORS -) +add_dependencies( + vulkan_siggraph + basic_vertex_shader + basic_fragment_shader + albedo_fragment_shader + solid_color_fragment_shader) + +target_compile_definitions( + vulkan_siggraph + PRIVATE GLFW_INCLUDE_NONE + # Points at the sample assets tutorial. + SIGGRAPH_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" + # tells the app from where to read SPIR - V files. + SIGGRAPH_SHADER_DIR="${SHADER_OUTPUT_DIR}" + # The Vulkan - Hpp definitions: enable dynamic dispatch loading and + # designated initializers. + VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 + VULKAN_HPP_NO_STRUCT_CONSTRUCTORS) # Allow source files to include local headers with #include "util.h". target_include_directories(vulkan_siggraph PRIVATE src) -target_include_directories(vulkan_siggraph SYSTEM PRIVATE external/nlohmann/single_include) -target_include_directories(vulkan_siggraph SYSTEM PRIVATE external/stb) -target_include_directories(vulkan_siggraph SYSTEM PRIVATE external/tinygltf) -# The sample uses C++23 library/features and Vulkan-Hpp designated initialization. +# The sample uses C++ 23. target_compile_features(vulkan_siggraph PRIVATE cxx_std_23) -# Link Vulkan, the window library, math helpers and bootstrap helpers -target_link_libraries(vulkan_siggraph - PRIVATE - Vulkan::Vulkan - glfw - glm::glm - vk-bootstrap::vk-bootstrap -) +# Link Vulkan, the window library, math helpers, and bootstrap helpers. +target_link_libraries( + vulkan_siggraph + PRIVATE Vulkan::Vulkan + glfw + glm::glm + vk-bootstrap::vk-bootstrap + nlohmann_json::nlohmann_json + stb::stb + tinygltf::tinygltf) # Use strict warnings so tutorial code stays clear and portable across compilers. if(MSVC) - target_compile_options(vulkan_siggraph PRIVATE /std:c++latest /W4 /permissive-) + target_compile_options(vulkan_siggraph PRIVATE /W4 /permissive-) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") - target_compile_options(vulkan_siggraph PRIVATE /clang:-Wno-missing-designated-field-initializers) + target_compile_options( vulkan_siggraph PRIVATE /clang:-Wno-missing-designated-field-initializers) endif() else() - target_compile_options(vulkan_siggraph PRIVATE -std=c++2c -Wall -Wextra -Wpedantic -Wno-missing-designated-field-initializers) + target_compile_options(vulkan_siggraph PRIVATE -Wall -Wextra -Wpedantic) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(vulkan_siggraph PRIVATE -Wno-missing-designated-field-initializers) + endif() endif() # In Visual Studio, make the tutorial executable the default startup project. diff --git a/attachments/siggraph2026_vk_tutorial/CMakePresets.json b/attachments/siggraph2026_vk_tutorial/CMakePresets.json index 2a65631c..d3ee18e8 100644 --- a/attachments/siggraph2026_vk_tutorial/CMakePresets.json +++ b/attachments/siggraph2026_vk_tutorial/CMakePresets.json @@ -13,18 +13,6 @@ "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } }, - { - "name": "clang-cl-debug", - "displayName": "Clang-CL Debug", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/clang-cl", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_C_COMPILER": "clang-cl", - "CMAKE_CXX_COMPILER": "clang-cl", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" - } - }, { "name": "vs2022-debug", "displayName": "Visual Studio 2022 x64 Debug", @@ -34,6 +22,16 @@ "value": "x64", "strategy": "set" } + }, + { + "name": "vs2026-debug", + "displayName": "Visual Studio 2026 x64 Debug", + "generator": "Visual Studio 18 2026", + "binaryDir": "${sourceDir}/build/vs2026", + "architecture": { + "value": "x64", + "strategy": "set" + } } ], "buildPresets": [ @@ -41,14 +39,15 @@ "name": "clang-debug", "configurePreset": "clang-debug" }, - { - "name": "clang-cl-debug", - "configurePreset": "clang-cl-debug" - }, { "name": "vs2022-debug", "configurePreset": "vs2022-debug", "configuration": "Debug" + }, + { + "name": "vs2026-debug", + "configurePreset": "vs2026-debug", + "configuration": "Debug" } ] } diff --git a/attachments/siggraph2026_vk_tutorial/README.adoc b/attachments/siggraph2026_vk_tutorial/README.adoc new file mode 100644 index 00000000..178ec8bb --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/README.adoc @@ -0,0 +1,115 @@ += SIGGRAPH 2026: How to write a Vulkan application + +This is a simple tutorial with the objective of explaining how to write a Vulkan application in 2026. +The tutorial employs the latest recommended best practices: + +* Descriptor heap +* Shader Object +* Dynamic Rendering + +It is intended for people new to Vulkan, and aims to showcase the main improvements since Vulkan 1.0 + +Shaders are written using Slang, a modern shading language hosted by the Khronos Group. + +See the https://github.com/KhronosGroup/Vulkan-Tutorial/blob/main/en/courses/siggraph2026_vk_tutorial[course page] for more information. + +[NOTE] +==== +ChatGPT and Codex were used to generate code for this tutorial. +However, all code has been reviewed, edited, and analyzed. +==== + +== Build + +=== Prerequisites + +* C++ 2023 tool chain +** Visual Studio 2022 (or newer) +** Clang 21 (or newer) and Ninja +* CMake 3.26 or newer +* Git +* Vulkan SDK +* +slangc+, either on +PATH+ or supplied with +-DSLANGC_EXECUTABLE=...+ + +[NOTE] +==== +The tutorial builds and runs on Windows and Linux. Android is not supported. +This tutorial requires a lot of modern Vulkan extensions, including +VK_EXT_descriptor_heap+ and +VK_EXT_shader_object+. +Old Vulkan drivers will not be able to run this tutorial. +==== + +=== Setup + +No submodule initialization is required. + +CMake uses git during setup to download the necessary third-party libraries. +This includes glTF assets for Sponza. + +=== Compile with Visual Studio (Windows) + +The repo includes a Visual Studio 2022 and Visual Studio 2026 presets: + +[source,bash] +---- +cmake --preset vs2022-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" + +cmake --preset vs2026-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" +---- + +This writes Visual Studio project files and the executable under +build/vs2022+ and +build/vs2026+. + +To build you can open +build/vs2026/vulkan_siggraph.slnx+ alternatively you can run in the console: + +[source,bash] +---- +cmake --build --preset vs2026-debug +---- + +=== Compile with Clang (Windows and Linux) + +The repo includes CMake presets to compile using clang: + +[source,bash] +---- +cmake --preset clang-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" +cmake --build --preset clang-debug +---- + +[NOTE] +==== ++-DSLANGC_EXECUTABLE+ is optional. If not provided, CMake will use the version of +slangc+ in +PATH+. +==== + +== Run + +[source,bash] +---- +.\build\vs2026\Debug\vulkan_siggraph.exe +.\build\clang\vulkan_siggraph.exe +---- + +== Files + +* +src/main.cpp+, +src/main.h+: Main application containing the Vulkan code. +* +src/util.cpp+, +src/util.h+: Collection of helper functions to make the tutorial easy to follow. Functions in these files parse reflection data, glTF objects, textures, and related inputs, so they are less relevant if your objective is to learn Vulkan. +* +shaders/+: Slang shaders. + +== License + +This SIGGRAPH tutorial sample is licensed under the link:LICENSE[MIT License]. +This license applies to the sample code and documentation in this directory. +Assets and third-party libraries remain under their own licenses. + +== Assets + +* *Sponza* - Cryengine Limited License Agreement (+LicenseRef-CRYENGINE-Agreement+). Building interior glTF scene used for lighting and rendering tests. See https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/Sponza[glTF Sample Assets]. +* *Vulkan logo* - Khronos trademark. The Vulkan logo is a trademark of The Khronos Group Inc. The MIT license does not grant trademark rights. See the https://www.khronos.org/legal/trademarks/[Khronos trademark guidelines]. + +== Third-party Libraries + +* *GLFW* - https://github.com/glfw/glfw[glfw/glfw]: Window creation and Vulkan surface integration (https://github.com/glfw/glfw/blob/master/LICENSE.md[zlib License]). +* *GLM* - https://github.com/g-truc/glm[g-truc/glm]: Header-only math library for vectors, matrices, and transforms (https://github.com/g-truc/glm/blob/master/copying.txt[MIT or Happy Bunny License]). +* *vk-bootstrap* - https://github.com/charles-lunarg/vk-bootstrap[charles-lunarg/vk-bootstrap]: Helper library for Vulkan instance, device, queue, and swapchain setup (https://github.com/charles-lunarg/vk-bootstrap/blob/main/LICENSE.txt[MIT License]). +* *nlohmann/json* - https://github.com/nlohmann/json[nlohmann/json]: Header-only JSON parser (https://github.com/nlohmann/json/blob/develop/LICENSE.MIT[MIT License]). +* *stb* - https://github.com/nothings/stb[nothings/stb]: Header-only utility library; this tutorial uses +stb_image+ for texture loading (https://github.com/nothings/stb/blob/master/LICENSE[MIT or Public Domain / Unlicense]). +* *tinygltf* - https://github.com/syoyo/tinygltf[syoyo/tinygltf]: Header-only glTF 2.0 loader (https://github.com/syoyo/tinygltf/blob/release/LICENSE[MIT License]). diff --git a/attachments/siggraph2026_vk_tutorial/README.md b/attachments/siggraph2026_vk_tutorial/README.md deleted file mode 100644 index 42b86050..00000000 --- a/attachments/siggraph2026_vk_tutorial/README.md +++ /dev/null @@ -1,135 +0,0 @@ - -# SIGGRAPH 2026: How to write a Vulkan application - -This is a simple tutorial with the objective of explaining how to write a Vulkan application in 2026. -The tutorial employs the latest recommended best practices for beginners: - -- Descriptor heap -- Shader Object -- Dynamic Rendering - -Shaders are written using Slang, a modern shading language hosted by the Khronos Group. - -See the [course page](https://github.com/KhronosGroup/Vulkan-Tutorial/blob/main/en/courses/siggraph2026_vk_tutorial) for more information. - -> [!NOTE] -> ChatGPT and Codex were used to generate code for this tutorial. -> However, all code has been reviewed, edited, and analyzed. - -## Build - -### Prerequisites - -- Visual Studio 2022 or Clang and Ninja -- CMake 3.26 or newer -- Vulkan SDK -- `slangc`, either on `PATH` or supplied with `-DSLANGC_EXECUTABLE=...` - -> [!NOTE] -> The tutorial builds and runs on Windows and Linux. Android is not supported. -> This tutorial requires a lot of modern Vulkan extensions, including `VK_EXT_descriptor_heap` and `VK_EXT_shader_object`. -> Old Vulkan drivers will not be able to run this tutorial. - -### Setup - -After cloning the repository, initialize the pinned dependencies with: - -```bash -python .\init_submodules.py -``` - -The `commit = ...` entries in `.gitmodules` are a project convention used by -`init_submodules.py` to pin each dependency. Plain Git ignores this field. -The Sponza asset submodule also uses `sparse-path = Models/Sponza` so the setup -script checks out only the Sponza folder from `glTF-Sample-Assets`. - -If you need to set up the dependencies manually, run the matching commands for -each dependency: - -```bash -git init - -git submodule add --force --name glfw https://github.com/glfw/glfw.git external/glfw -git submodule add --force --name glm https://github.com/g-truc/glm.git external/glm -git submodule add --force --name vk-bootstrap https://github.com/charles-lunarg/vk-bootstrap.git external/vk-bootstrap -git submodule add --force --name nlohmann https://github.com/nlohmann/json.git external/nlohmann -git submodule add --force --name stb https://github.com/nothings/stb.git external/stb -git submodule add --force --name tinygltf https://github.com/syoyo/tinygltf.git external/tinygltf - -git clone --filter=blob:none --sparse --no-checkout https://github.com/KhronosGroup/glTF-Sample-Assets.git data/external/sponza -git -C data/external/sponza sparse-checkout set Models/Sponza -git -C data/external/sponza checkout 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf -git submodule add --force --name gltf-sample-assets https://github.com/KhronosGroup/glTF-Sample-Assets.git data/external/sponza -git submodule absorbgitdirs data/external/sponza - -git submodule init -git submodule update --init --recursive - -git -C external/glfw checkout b00e6a8a88ad1b60c0a045e696301deb92c9a13e -git -C external/glm checkout 6f14f4792a0cde5d0cf2c910506724d61cb95834 -git -C external/vk-bootstrap checkout 5ca6780498864ae4c12f3a594ee6a6c5133d4ce0 -git -C external/nlohmann checkout 4fad4468974a7b1b26d374b1c5955d2ac7d449b0 -git -C external/stb checkout 31c1ad37456438565541f4919958214b6e762fb4 -git -C external/tinygltf checkout d31c16e333a6c8d593cad43f325f4e1825dd4776 -git -C data/external/sponza sparse-checkout set Models/Sponza -git -C data/external/sponza checkout 2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf -``` - -### Compile with Visual Studio (Windows) - -The repo includes a Visual Studio 2022 x64 preset: - -```bash -cmake --preset vs2022-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" -``` - -This writes Visual Studio project files and the executable under `build/vs2022`. - -To build: -```bash -cmake --build --preset vs2022-debug -``` - -### Compile with Clang (Windows and Linux) - -The repo includes CMake presets for this workflow: - -```bash -cmake --preset clang-debug -DSLANGC_EXECUTABLE="C:/VulkanSDK/1.4.341.1/Bin/slangc.exe" -cmake --build --preset clang-debug -``` - -> [!NOTE] -> `-DSLANGC_EXECUTABLE` is optional. If not provided, CMake will use the version of `slangc` in `PATH`. - -## Run - -```bash -.\build\vs2022\Debug\vulkan_siggraph.exe -``` - -## Files - -- `src/main.cpp`, `src/main.h`: Main application containing the Vulkan code. -- `src/util.cpp`, `src/util.h`: Collection of helper functions to make the tutorial easy to follow. Functions in these files parse reflection data, glTF objects, textures, and related inputs, so they are less relevant if your objective is to learn Vulkan. -- `shaders/`: Slang shaders. - -## License - -This SIGGRAPH tutorial sample is licensed under the [MIT License](LICENSE). -This license applies to the sample code and documentation in this directory. -Assets and third-party libraries remain under their own licenses. - -## Assets - -- **Sponza** - Cryengine Limited License Agreement (`LicenseRef-CRYENGINE-Agreement`). Building interior glTF scene used for lighting and rendering tests. See [glTF Sample Assets](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/Sponza). -- **Vulkan logo** - Khronos trademark. The Vulkan logo is a trademark of The Khronos Group Inc. The MIT license does not grant trademark rights. See the [Khronos trademark guidelines](https://www.khronos.org/legal/trademarks/). - -## Third-party Libraries - -- **GLFW** - [glfw/glfw](https://github.com/glfw/glfw): Window creation and Vulkan surface integration ([zlib License](https://github.com/glfw/glfw/blob/master/LICENSE.md)). -- **GLM** - [g-truc/glm](https://github.com/g-truc/glm): Header-only math library for vectors, matrices, and transforms ([MIT or Happy Bunny License](https://github.com/g-truc/glm/blob/master/copying.txt)). -- **vk-bootstrap** - [charles-lunarg/vk-bootstrap](https://github.com/charles-lunarg/vk-bootstrap): Helper library for Vulkan instance, device, queue, and swapchain setup ([MIT License](https://github.com/charles-lunarg/vk-bootstrap/blob/main/LICENSE.txt)). -- **nlohmann/json** - [nlohmann/json](https://github.com/nlohmann/json): Header-only JSON parser ([MIT License](https://github.com/nlohmann/json/blob/develop/LICENSE.MIT)). -- **stb** - [nothings/stb](https://github.com/nothings/stb): Header-only utility library; this tutorial uses `stb_image` for texture loading ([MIT or Public Domain / Unlicense](https://github.com/nothings/stb/blob/master/LICENSE)). -- **tinygltf** - [syoyo/tinygltf](https://github.com/syoyo/tinygltf): Header-only glTF 2.0 loader ([MIT License](https://github.com/syoyo/tinygltf/blob/release/LICENSE)). diff --git a/attachments/siggraph2026_vk_tutorial/cmake/fetch_git.cmake b/attachments/siggraph2026_vk_tutorial/cmake/fetch_git.cmake new file mode 100644 index 00000000..0e5b8ad2 --- /dev/null +++ b/attachments/siggraph2026_vk_tutorial/cmake/fetch_git.cmake @@ -0,0 +1,80 @@ +# Copyright (c) 2026, Khronos Group and contributors +# +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +#Function to clone or update one pinned Git checkout + +# Check script arguments. +foreach(required_var IN ITEMS GIT_EXECUTABLE GIT_REPOSITORY GIT_TAG SOURCE_DIR) + if(NOT DEFINED ${required_var} OR "${${required_var}}" STREQUAL "") + message(FATAL_ERROR "fetch_git.cmake requires ${required_var}") + endif() +endforeach() + +# Run one Git command and stop configuration immediately if it fails. +function(run_git) + execute_process( COMMAND "${GIT_EXECUTABLE}" ${ARGV} COMMAND_ERROR_IS_FATAL ANY) +endfunction() + +# SOURCE_DIR is the final checkout path requested by CMakeLists.txt. +# Create its parent directory before cloning into it. +get_filename_component(source_parent "${SOURCE_DIR}" DIRECTORY) +file(MAKE_DIRECTORY "${source_parent}") + +set(created_repository OFF) +if(NOT EXISTS "${SOURCE_DIR}/.git") + message( STATUS "GIT_FETCH: Creating repository ${SOURCE_DIR} from ${GIT_REPOSITORY}") + set(clone_args clone --no-checkout) + if(DEFINED SPARSE_PATH AND NOT "${SPARSE_PATH}" STREQUAL "") + list(APPEND clone_args --filter=blob:none --sparse) + endif() + run_git(${clone_args} "${GIT_REPOSITORY}" "${SOURCE_DIR}") + set(created_repository ON) +endif() + +if(DEFINED SPARSE_PATH AND NOT "${SPARSE_PATH}" STREQUAL "") + message( STATUS "GIT_FETCH: Setting sparse checkout for ${SOURCE_DIR}: ${SPARSE_PATH}") + run_git(-C "${SOURCE_DIR}" sparse-checkout set "${SPARSE_PATH}") +endif() + +# Read the current HEAD commit. A newly-created --no-checkout clone can already +# point HEAD at GIT_TAG, but still needs checkout to populate its working tree. +execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --verify HEAD + OUTPUT_VARIABLE current_commit + RESULT_VARIABLE head_result + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + +# If the checkout already points at the pinned commit, leave it untouched. +# If the repository is created it will be empty until checkout. +if(NOT created_repository AND current_commit STREQUAL GIT_TAG) + message(STATUS "GIT_FETCH: Skipping ${SOURCE_DIR}; already at ${GIT_TAG}") +else() + run_git(-C "${SOURCE_DIR}" fetch --filter=blob:none origin) + message(STATUS "GIT_FETCH: Checking out ${SOURCE_DIR} at ${GIT_TAG}") + run_git(-C "${SOURCE_DIR}" checkout --detach "${GIT_TAG}") + execute_process( + COMMAND "${GIT_EXECUTABLE}" -C "${SOURCE_DIR}" rev-parse --verify HEAD + OUTPUT_VARIABLE checked_out_commit + COMMAND_ERROR_IS_FATAL ANY + OUTPUT_STRIP_TRAILING_WHITESPACE) + message( STATUS "GIT_FETCH: Checked out ${SOURCE_DIR} at ${checked_out_commit}") +endif() diff --git a/attachments/siggraph2026_vk_tutorial/init_submodules.py b/attachments/siggraph2026_vk_tutorial/init_submodules.py deleted file mode 100644 index 7ee23a39..00000000 --- a/attachments/siggraph2026_vk_tutorial/init_submodules.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright (c) 2026, Khronos Group and contributors -# -# SPDX-License-Identifier: MIT -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from pathlib import Path -import subprocess -import sys - - -ROOT = Path(__file__).resolve().parent - - -def run(args): - return subprocess.run(args, cwd=ROOT, text=True, check=True, capture_output=True) - - -def git_config(key): - result = subprocess.run( - ["git", "config", "--file", ".gitmodules", "--get", key], - cwd=ROOT, - text=True, - capture_output=True, - ) - if result.returncode != 0: - raise RuntimeError(f"Missing .gitmodules key: {key}") - return result.stdout.strip() - - -def git_config_optional(key): - result = subprocess.run( - ["git", "config", "--file", ".gitmodules", "--get", key], - cwd=ROOT, - text=True, - capture_output=True, - ) - if result.returncode != 0: - return None - value = result.stdout.strip() - return value if value else None - - -def submodule_names(): - paths = run([ - "git", - "config", - "--file", - ".gitmodules", - "--get-regexp", - r"^submodule\..*\.path$", - ]).stdout.splitlines() - - names = [] - for line in paths: - key, _ = line.split(maxsplit=1) - names.append(key.removeprefix("submodule.").removesuffix(".path")) - return names - - -def normalize_sparse_path(path): - return path.replace("\\", "/").strip().strip("/") - - -def current_sparse_checkout_paths(path): - result = subprocess.run( - ["git", "-C", path, "sparse-checkout", "list"], - cwd=ROOT, - text=True, - capture_output=True, - ) - if result.returncode != 0: - return None - return [normalize_sparse_path(line) for line in result.stdout.splitlines() if line.strip()] - - -def configure_sparse_checkout(path, sparse_path): - desired_path = normalize_sparse_path(sparse_path) - current_paths = current_sparse_checkout_paths(path) - if current_paths is not None and desired_path in current_paths: - print(f"Sparse checkout for {path} already configured: {sparse_path}") - return False - - print(f"Configuring sparse checkout for {path}: {sparse_path}") - run(["git", "-C", path, "sparse-checkout", "set", sparse_path]) - return True - - -def ensure_sparse_submodule(name, path, url, commit, sparse_path): - target = ROOT / path - git_dir = target / ".git" - - if target.exists(): - if not git_dir.exists(): - raise RuntimeError(f"{path} exists but is not a Git checkout or submodule") - print(f"Submodule {name} exists at {path}") - configure_sparse_checkout(path, sparse_path) - return False - - print(f"Adding sparse submodule {name} at {path}") - target.parent.mkdir(parents=True, exist_ok=True) - run(["git", "clone", "--filter=blob:none", "--sparse", "--no-checkout", url, path]) - configure_sparse_checkout(path, sparse_path) - run(["git", "-C", path, "checkout", commit]) - run(["git", "submodule", "add", "--force", "--name", name, url, path]) - run(["git", "submodule", "absorbgitdirs", path]) - return True - - -def ensure_submodule(name, path, url, commit, sparse_path=None): - if sparse_path: - return ensure_sparse_submodule(name, path, url, commit, sparse_path) - - target = ROOT / path - git_dir = target / ".git" - - if target.exists(): - if not git_dir.exists(): - raise RuntimeError(f"{path} exists but is not a Git checkout or submodule") - print(f"Submodule {name} exists at {path}") - return False - - print(f"Adding submodule {name} at {path}") - run(["git", "submodule", "add", "--force", "--name", name, url, path]) - return True - - -def main(): - if not (ROOT / ".git").exists(): - run(["git", "init"]) - else: - print(f"Skippingg git initalization. {ROOT}.git folder already exists") - - names = submodule_names() - if not names: - raise RuntimeError("No submodules found in .gitmodules") - - init_submodules = False - for name in names: - path = git_config(f"submodule.{name}.path") - url = git_config(f"submodule.{name}.url") - commit = git_config(f"submodule.{name}.commit") - sparse_path = git_config_optional(f"submodule.{name}.sparse-path") - if not commit: - raise RuntimeError(f"Missing commit for submodule {name}") - if ensure_submodule(name, path, url, commit, sparse_path): - init_submodules=True - - if init_submodules: - print(f"initalization of git submodules at {ROOT}") - run(["git", "-C", f"{ROOT}", "submodule", "init"]) - run(["git", "-C", f"{ROOT}", "submodule", "update", "--recursive"]) - - for name in names: - path = git_config(f"submodule.{name}.path") - commit = git_config(f"submodule.{name}.commit") - sparse_path = git_config_optional(f"submodule.{name}.sparse-path") - if sparse_path: - configure_sparse_checkout(path, sparse_path) - current_commit = run(["git", "-C", path, "rev-parse", "HEAD"]).stdout.strip() - if current_commit == commit: - print(f"Skipping {path} update. Already at correct commit") - else: - print(f"Updating {path} from {current_commit} to {commit}") - run(["git", "-C", path, "checkout", commit]) - run(["git", "-C", path, "submodule", "update", "--init", "--recursive"]) - -if __name__ == "__main__": - try: - main() - except (subprocess.CalledProcessError, RuntimeError) as error: - if isinstance(error, subprocess.CalledProcessError): - print(error.stderr or error, file=sys.stderr) - sys.exit(error.returncode) - print(error, file=sys.stderr) - sys.exit(1) diff --git a/attachments/siggraph2026_vk_tutorial/shaders/albedo.frag.slang b/attachments/siggraph2026_vk_tutorial/shaders/albedo.frag.slang index d518d007..cf98491d 100644 --- a/attachments/siggraph2026_vk_tutorial/shaders/albedo.frag.slang +++ b/attachments/siggraph2026_vk_tutorial/shaders/albedo.frag.slang @@ -48,11 +48,11 @@ Sampler2D g_albedoTexture; // In Slang we can use HLSL annotations to communicate attributes between vertex and fragment shaders. struct FragmentInput { - float3 worldPosition : TEXCOORD2; - float3 normal : NORMAL; - float3 tangent : TANGENT; - float tangentSign : TEXCOORD1; - float2 uv : TEXCOORD0; + float3 worldPosition; + float3 normal; + float3 tangent; + float tangentSign; + float2 uv; }; // Simple illumination function @@ -77,8 +77,7 @@ float3 illuminate(float3 albedo, float3 worldPosition, float3 normal, LightData } [shader("fragment")] -float4 fragmentMain(FragmentInput input) : SV_Target -{ +float4 fragmentMain(FragmentInput input) { // The albedo combined sampled image is selected by HEAP_WITH_PUSH_INDEX. float4 texel = g_albedoTexture.Sample(input.uv); if (texel.a < 0.5) { @@ -88,11 +87,17 @@ float4 fragmentMain(FragmentInput input) : SV_Target float3 normal = normalize(input.normal); if (texel.a < 0.0) { - // HACK: Disable performance warning: + // HACK: Disable the performance warning by faking the use of all inputs. + // If we remove this code, we get this warning when running with validation layers: // [VK_SHADER_STAGE_VERTEX_BIT] has an Output value declared but there is no corresponding Input declared in - // [VK_SHADER_STAGE_FRAGMENT_BIT]. This is not invalid, but the write to the unused Output is discarded, causing - // a performance validation message. Proper solution is to have a custom vertex shader, but this is - // intentionally avoided to keep the tutorial simple. + // [VK_SHADER_STAGE_FRAGMENT_BIT]. + // + // This is not invalid; it is just a performance warning telling us that the write to the unused output is + // discarded. This can cause a performance loss on some GPUs, especially when using unlinked shader objects, + // since they might disable cross-stage shader optimizations. + // + // We should aim for a proper solution. For example, we could have a custom vertex shader that skips writing the + // unused inputs, but this is intentionally avoided to keep the tutorial simple. normal += input.tangent * 0.001 + input.tangentSign * 0.001; } return float4(illuminate(texel.rgb, input.worldPosition, normalize(normal), g_pointLight), 1.0); diff --git a/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang b/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang index 6eabcc82..c4cee240 100644 --- a/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang +++ b/attachments/siggraph2026_vk_tutorial/shaders/basic.frag.slang @@ -52,11 +52,11 @@ Sampler2D g_normalTexture; // In Slang we can use HLSL annotations to communicate attributes between vertex and fragment shaders. struct FragmentInput { - float3 worldPosition : TEXCOORD2; - float3 normal : NORMAL; - float3 tangent : TANGENT; - float tangentSign : TEXCOORD1; - float2 uv : TEXCOORD0; + float3 worldPosition; + float3 normal; + float3 tangent; + float tangentSign; + float2 uv; }; // Transforms geometry normal using a normal texture @@ -93,8 +93,7 @@ float3 illuminate(float3 albedo, float3 worldPosition, float3 normal, LightData } [shader("fragment")] -float4 fragmentMain(FragmentInput input) : SV_Target -{ +float4 fragmentMain(FragmentInput input) { // The albedo and normal combined sampled images are selected by HEAP_WITH_PUSH_INDEX. float4 texel = g_albedoTexture.Sample(input.uv); if (texel.a < 0.5) { diff --git a/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang b/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang index 8fba4a88..fcd8e6fa 100644 --- a/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang +++ b/attachments/siggraph2026_vk_tutorial/shaders/basic.vert.slang @@ -54,24 +54,24 @@ StructuredBuffer g_objects; // Automatic deduction of locationId using Slang reflection might be preferred in most use cases. struct VertexInput { [[vk::location(0)]] - float3 position : POSITION; + float3 position; [[vk::location(1)]] - float3 normal : NORMAL; + float3 normal; [[vk::location(2)]] - float4 tangent : TANGENT; + float4 tangent; [[vk::location(3)]] - float2 uv : TEXCOORD0; + float2 uv; }; // Send data from vertex shader to fragment shader. // Rasterization uses SV_Position while the remaining fields are interpolated for the fragment shader. struct VertexOutput { float4 position : SV_Position; - float3 worldPosition : TEXCOORD2; - float3 normal : NORMAL; - float3 tangent : TANGENT; - float tangentSign : TEXCOORD1; - float2 uv : TEXCOORD0; + float3 worldPosition; + float3 normal; + float3 tangent; + float tangentSign; + float2 uv; }; [shader("vertex")] diff --git a/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang b/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang index 1e51f614..3d15f7b0 100644 --- a/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang +++ b/attachments/siggraph2026_vk_tutorial/shaders/solid_color.frag.slang @@ -31,24 +31,29 @@ struct SolidColorData { ConstantBuffer g_solidColor; struct FragmentInput { - float3 worldPosition : TEXCOORD2; - float3 normal : NORMAL; - float3 tangent : TANGENT; - float tangentSign : TEXCOORD1; - float2 uv : TEXCOORD0; + float3 worldPosition; + float3 normal; + float3 tangent; + float tangentSign; + float2 uv; }; -[shader("fragment")] -float4 fragmentMain(FragmentInput input) : SV_Target +[shader("fragment")] float4 fragmentMain(FragmentInput input) : SV_Target { float4 color = g_solidColor.color; - // HACK: Disable performance warning: - // [VK_SHADER_STAGE_VERTEX_BIT] has an Output value declared but there is no corresponding Input declared in - // [VK_SHADER_STAGE_FRAGMENT_BIT]. This is not invalid, but the write to the unused Output is discarded, causing a - // performance validation message. Proper solution is to have a custom vertex shader, but this is intentionally - // avoided to keep the tutorial simple. if (color.a < 0.0f) { + // HACK: Disable the performance warning by faking the use of all inputs. + // If we remove this code, we get this warning when running with validation layers: + // [VK_SHADER_STAGE_VERTEX_BIT] has an Output value declared but there is no corresponding Input declared in + // [VK_SHADER_STAGE_FRAGMENT_BIT]. + // + // This is not invalid; it is just a performance warning telling us that the write to the unused output is + // discarded. This can cause a performance loss on some GPUs, especially when using unlinked shader objects, + // since they might disable cross-stage shader optimizations. + // + // We should aim for a proper solution. For example, we could have a custom vertex shader that skips writing the + // unused inputs, but this is intentionally avoided to keep the tutorial simple. color.xy += input.uv * 0.001; color.w += input.tangentSign * 0.001; color.xyz += input.tangent * 0.001;