Building a Triangle with Vulkan and C++ in Fedora 37
Navigation⌗
- Beginning the Journey
- Pre-requisites
- The Material
- Downloading The Boilerplate
- VulkanSDK
- DNF
- CMake
- Testing Vulkan
- Starting Line
- Window Notes
- Initial Directory Structure
- Vulkan Window
- FirstApp Class
- JdeWindow Class
- Main Function
- Launching the Window
- The Hard Part
- Full Directory Structure
- Shader Chain
- Shader Compilation
- Starting The Triangle
- Revamped first_app.hpp
- Revamped first_app.cpp
- Graphics Pipeline Configuration jde_pipeline.hpp
- Graphics Pipeline Configuration jde_pipeline.cpp
- Revamped jde_window.hpp
- Revamped jde_window.cpp
- Build The Triangle
- Launch The Triangle!
Beginning the Journey⌗
I’ve been looking at Vulkan from afar for 6 months while I honed my C++ skills and worked through SFML, GLFW, and OpenGL, along with a few windowing frameworks like ImGui and GTK3, but all of that work just barely prepared me for this most arduous of tasks… The legendary Vulkan Triangle! Settle in, this is going to take some time to even read through, let alone work through!
I sat down about 8 hours ago yesterday, now at the time of finishing the post, thinking this might not be so bad, and in some ways it’s not that bad, but comparing it to everything I’ve done till now, it is the hardest thing I’ve had to comprehend in programming. What makes a Triangle less than simple in Vulkan is that by simply starting a Vulkan project you’re signing up for plenty of up-front thinking through the architecture of what you’re building as well as filling in the fields that Vulkan expects of Graphical Applications both small and massive. The tried and true OpenGL was pretty dang quick to get up and running and gave you a sense of “I can do this!”, but then at every turn there was something to add to your API, and if you didn’t know it was coming it could derail your project to the point of no return. Having gone through a few of the stumbling points with OpenGL I had some idea of what I was getting into with Vulkan, but not nearly enough to think twice about sitting down with it tonight. TL;DR: Vulkan forces you to double check your vision before stepping into the pitfalls you’ll surely encounter with OpenGL.
All of the content from this post summarizes the first 6 videos of Brendan Galea’s Vulkan Game Engine YouTube Series.
Pre-requisites⌗
This must be the longest list of pre-requisites I’ve seen in a post, let-alone a post I wrote, but it’s all of the things you need to get going on Fedora Linux version 37 at the time of writing this.
The Material⌗
I didn’t set out to follow a tutorial, but I absolutely had to, and everyone recommended Brendan Galea’s Vulkan Game Engine Tutorial over the last few months of casually looking into this. I decided that it was going to be the quickest way to get up and running in a manner that would allow me to continue learning and would align with my overall plans for Vulkan, which is to incorporate it into an game engine for myself to learn with.
I’m going to use Brendan’s tutorial as reference for some of the points I talk about, but mostly I’m going to make his process more specific to Linux and even more specifically Fedora. Fedora makes for a great workstation, so that’s where I’ve been spending my last few months.
Download These And Make Edits Accordingly⌗
Brendan has 4 files hosted that you’ll need to download and follow his short instructions on what to change in those files, these links will take you to the timestamp in the videos, as well as to the download location within the description of the videos.
VulkanSDK⌗
The biggest thing to get going up front is to Download the VulkanSDK and get it properly set up in your environment for use with glslc.
When you get the SDK - SDK Installer vulkansdk-linux-x86_64-1.3.236.0.tar.gz (at the time of writing this) file downloaded, follow the next few commands to get the ball rolling on this setup process. NOTE: There’s a script in the tar archive that is supposed to make it easy to install, but it didn’t work for me, so I created a new directory at ~/VulkanSDK and moved our tar into it:
mkdir ~/VulkanSDK
cd ~/VulkanSDK
tar -xzvf vulkansdk-linux-x86_64-1.3.236.0.tar.gz
cd 1.3.236.0/x86_64
pwd
pwd will output the path you’ll need for the $VULKAN_SDK environment variable
/home/USERNAME/VulkanSDK/1.3.236.0/x86_64
From there you can copy that path and use it for the following instructions.
since most people are using BASH I’ll post the instructions for that first,
but I’ll go into detail about the fish solution as well!
export VULKAN_SDK="/home/USERNAME/VulkanSDK/1.3.236.0/x86_64/"
export PATH="$VULKAN_SDK/bin:$PATH"
Run those in your terminal to add them temporarily, or add them to your ~/.bashrc or ~/.bash_profile or somewhere else you can think of.
Next up the solution that Fish Shell users have to live with:
set -gx VULKAN_SDK "/home/USERNAME/VulkanSDK/1.3.236.0/x86_64/"
fish_add_path $VULKAN_SDK/bin/
those are now added permenantly to your path, you can overwrite them them with the same command if you make a typo, or clear them entirely with another set command.
You should now be able to run glslc⌗
test this by issuing a simple:
glslc --version
and you should see:
shaderc v2022.4 v2022.4
spirv-tools v2022.5-dev v2022.4-28-gd9446130
glslang 11.1.0-605-g728c6895
Target: SPIR-V 1.0
DNF⌗
Now that the manual setup is out of the way is out of the way it’s time to get our development environment up and going!
dnf all the things!
sudo dnf install \
@"Development Tools" \
vulkan-headers \
vulkan-loader-devel \
vulkan-tools \
vulkan-validation-layers \
vulkan-devel \
libcxx-devel \
libcxx \
glm-devel \
cmake \
libpng-devel \
wayland-devel \
libpciaccess-devel \
libX11-devel \
libXpresent \
libxcb \
xcb-util \
libxcb-devel \
libXrandr-devel \
xcb-util-keysyms-devel \
xcb-util-wm-devel \
python3 \
git \
lz4-devel \
libzstd-devel \
python3-distutils-extra \
qt \
gcc-g++ \
wayland-protocols-devel \
ninja-build \
python3-jsonschema \
qt5-qtbase-devel \
qt \
xinput \
libXinerama
CMake⌗
My preferred build generator is CMake even with its blemishes. I’ve used a few dirty tricks to get this going quickly.
CmakeLists.txt⌗
# CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(Vulkan-Starter)
# Set C++ standard to a proper standard
set(CMAKE_CXX_STANDARD 20)
# Add the GLFW and Vulkan libraries
find_package(glfw3 REQUIRED)
find_package(Vulkan REQUIRED)
find_package(glm REQUIRED)
# Copy contents of src/shaders to the binary location
add_custom_target(copy_assets
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_LIST_DIR}/src/shaders
${CMAKE_CURRENT_BINARY_DIR}/src/shaders
)
# Dirty way to add all source files in the ./src directory
file(GLOB_RECURSE SRC_FILES "src/*.cpp")
# Create the executable
add_executable(Vulkan-Starter ${SRC_FILES})
# Link our executable against the libraries
target_link_libraries(Vulkan-Starter glfw)
target_link_libraries(Vulkan-Starter Vulkan::Vulkan)
target_link_libraries(Vulkan-Starter glm::glm)
add_dependencies(Vulkan-Starter copy_assets)
A
CMakeLists.txtfile is used to configure C and C++ projects with CMake, ours is called “Vulkan-Starter”.
-
This config sets the C++ standard to version 20, which is the latest version of C++ for now! I’m impatiently waiting for C++23 this year!
-
We’ll use the
find_packagecommand to locate and include theGLFW,VulkanandGLMlibraries, which are required for theproject. -
Create a custom target that I’ve named
copy_assetswhich copies the contents of thesrc/shadersdirectory to the binary directory, the directory where the executable gets built. -
I opted to use
file(GLOB_RECURSE SRC_FILES "src/*.cpp")to add all source files in thesrc/directory to theSRC_FILESvariable… This is not a recommended option for modern CMake, but it does work, and before release you’d want to do a full revamp on theCMakeLists.txtfile anyway. -
Finally we create an executable called
Vulkan-Starterusing theadd_executablecommand and links it against theGLFW,Vulkan, andGLMlibraries using thetarget_link_librariescommand. -
The way that assets are handled is with a dependency between the
executableand the custom targetcopy_assetsusing theadd_dependenciescommand so that the assets are copied before the executable is built.
Testing Vulkan⌗
This file can be copied to use as a test for your environment. If you don’t have any errors then you’re ready to move on!
main.cpp⌗
// main.cpp
#define GLFW_INCLUDE_VULKAN
#include "GLFW/glfw3.h"
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include "glm/mat4x4.hpp"
#include "glm/vec4.hpp"
#include <iostream>
int main()
{
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan Window", nullptr, nullptr);
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
std::cout << extensionCount << " extensions supported\n";
glm::mat4 matrix;
glm::vec4 vec;
auto test = matrix * vec;
while (!glfwWindowShouldClose(window))
{
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
This is just a simple introduction to the world of Vulkan graphics programming! Ultimately this file should open a GLFW window.
-
Our entrypoint starts by initializing the
GLFWlibrary, which we use to create a window that will serve as the canvas for our Vulkan graphics. We then specify that we want to use the Vulkan client API, and create a window with a width of 800 pixels, a height of 600 pixels, and the title “Vulkan Window”. -
Next, we use the
vkEnumerateInstanceExtensionPropertiesfunction to determine the number of extensions supported by the current system. This information is then output to the console. My number was 20, but yours could be anything depending on your machine. -
We also utilize the
GLMlibrary for some matrix and vector operations, specifically creating a 4x4 matrix and a 4-element vector, and then performing a matrix-vector multiplication. -
Finally, we enter a loop that repeatedly polls for events and continues running as long as the window remains open. Once the window is closed, we clean up by destroying the window and shutting down
GLFW. -
Overall, this program serves as a basic introduction to some key concepts and libraries in the world of Vulkan programming, and is a great starting point for our upcoming triangle.
The Starting Line⌗

SPIR-V is the format that Vulkan expects for our shaders which is why we had to download VulkanSDK and get glslc added to the path
The CMakeLists.txt sets up everything our project will need
and the main.cpp file utilizes GLFW to create a Window and logs the number of extensions supported by vkEnumerateInstanceExtensionProperties
which in my case is 20 extensions supported and is the only use for this file.
Since the purpose of this main.cpp was to simply test our installation you may now remove all of the code from it and follow along with Brendan Galea’s Vulkan Game Engine Tutorial series on YouTube
Let’s Make Some Moves!⌗
Getting a window up and running with Vulkan is exactly the same as it would be with OpenGL and GLFW. I am very glad to have had a chance to play with GLFW when I did to smooth out this process!
I always prefer to start from the last file written and work my way backwards to the first. It usually layers properly in my brain that way, so we’ll be starting with first_app.cpp and work our way backwards ending with main.cpp.
Code Notes⌗
- anything you see that has
jdeorJdeis something that I changed, and is something that YOU can also change! In the videos you’ll notice these are originallylveandLvebut you should change these to something for yourself as it helps with the learning process. - every translation unit is namespaced with
jdewhich was also originallylvein the tutorial
we’re starting with a small directory structure that will just create a GLFW window:
├── CMakeLists.txt BUILDS PROJECT
└── src
├── first_app.cpp
├── first_app.hpp CLASS "FirstApp"
├── jde_window.cpp
├── jde_window.hpp CLASS "JdeWindow"
└── main.cpp ENTRY POINT
- remember it’s totally encouraged to change anything named
jdeorJdeto your liking, but the two are case sensitve!
Starting The Vulkan Window⌗
The FirstApp Class⌗
calls
FirstApp::run()in thejdenamespace which contains awhileloop that repeatedly callsglfwPollEvents()until thejdeWindowmember variable of theFirstAppclass returnstruefor theshouldClose()method. This is ourGame Loop.
first_app.cpp⌗
// first_app.cpp
#include "first_app.hpp"
namespace jde {
void FirstApp::run()
{
while (!jdeWindow.shouldClose())
{
glfwPollEvents();
}
}
}
first_app.hpp⌗
// first_app.hpp
#pragma once
#include "jde_window.hpp"
namespace jde {
class FirstApp {
public:
static constexpr int WIDTH = 800;
static constexpr int HEIGHT = 600;
void run();
private:
JdeWindow jdeWindow{WIDTH, HEIGHT, "Hello Vulkan!"};
};
}
note that we are including jde_window.hpp in the header in this file; the header for our FirstApp class in the jde namespace. This is where we define the WIDTH and HEIGHT of the application as static constexpr int, and at a resolution of 800x600. Finally the private method jdeWindow is initialized with our height and width when run() is called and our game loop begins;
The JdeWindow Class⌗
jde_window.hpp⌗
#pragma once
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <string>
namespace jde {
class JdeWindow {
public:
JdeWindow(int w, int h, std::string name);
~JdeWindow();
JdeWindow(const JdeWindow &) = delete;
JdeWindow &operator=(const JdeWindow &) = delete;
bool shouldClose() {
return glfwWindowShouldClose(window);
}
private:
void initWindow();
const int width;
const int height;
std::string windowName;
GLFWwindow *window;
};
}
this is our window class JdeWindow, its constructor takes a w, h, and a name which are used to initialize the width, height, and name of the window. We properly handle the available constructors, and the destructor destroys the window and terminates GLFW. The class has deleted copy constructor and copy assignment operator, which means that instances of this class cannot be copied. This is where our game loop is requested to terminate. JdeWindow has a public method called shouldClose(), which returns a bool indicating whether the window has been closed or not. Finally there is also one private method initWindow() that actually initializes the window. The private variables store width, height, and the name of the GLFW window.
jde_window.cpp⌗
// jde_window.cpp
#include "jde_window.hpp"
namespace jde {
JdeWindow::JdeWindow(int w, int h, std::string name):
width{w}, height{h}, windowName{name}
{
initWindow();
}
JdeWindow::~JdeWindow()
{
glfwDestroyWindow(window);
glfwTerminate();
}
void JdeWindow::initWindow()
{
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(width, height, windowName.c_str(), nullptr, nullptr);
}
}
jde_window.cppcode starts by including thejde_window.hppheader file, which contains declarations for theJdeWindowclass.- our namespace
jdeis defined. - The
JdeWindowclass is defined, which has a constructor that takes in three parameters:width,height, and a stringname. - In the
constructor, the input parameters are assigned to the member variables of the class. Then the functioninitWindow()is called. - A
destructoris defined, which is called when theJdeWindowobject is destroyed. It destroys the window and terminatesGLFW. - The
initWindow()function is defined, which initializesGLFWand sets some window hints. - The
GLFWlibrary is initialized. - The
GLFW_CLIENT_APIhint is set toGLFW_NO_APIto indicate that no graphics API is used - The
GLFW_RESIZABLEhint is set toGLFW_FALSEto indicate that the window is not resizable - A window is created with the specified
width,heightandname. - The window pointer is assigned to the window member variable.
Making Entry in main.cpp!⌗
We now get to hit the
run()command we saw very first! This is themain()function of the application, which is the entry point of our window!
#include "first_app.hpp"
#include <cstdlib>
#include <iostream>
#include <stdexcept>
int main()
{
jde::FirstApp app{};
try {
app.run();
} catch (const std::exception &e) {
std::cerr << e.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Our entrypoint creates an instance of our FirstApp class, which was defined in first_app.hpp, and calls run(); which is responsible for executing our main loop aka game loop, which just keeps running as long as the window is open. This loop keeps running as long as the window is open, by polling the events from the window. I like to use the powerful C++ standard when I can and haven’t had the opportunity to use try-catch blocks in a real-world application yet, so this tutorial has me happy for that. The main function also includes a try-catch block that catches any exceptions that are thrown by the run() method. If an exception is caught, the error message is printed to the standard error output and the program exits with the failure status code: EXIT_FAILURE; otherwise, if the program exits without an exception, it will return the EXIT_SUCCESS status code.
Opening the Window to the World of Vulkan!⌗
Run the following CMake command if you’re not using something like Emacs or VS Code:
cmake -B ./build -S .
and you should see around the same output as:
-- The C compiler identification is GNU 12.2.1
-- The CXX compiler identification is GNU 12.2.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found Vulkan: /lib64/libvulkan.so (found version "1.3.216") found components: glslc glslangValidator
-- Configuring done
-- Generating done
-- Build files have been written to: /home/mac/Documents/Projects/C++/Vulkan/build
changing into the build directory and issuing a
makecommand is next:
cd build
make
or I found that you can run make from anywhere using the -C flag make -C build
in any case you should see something similar to the following output:
[ 0%] Built target copy_assets
[ 14%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/first_app.cpp.o
[ 28%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_device.cpp.o
[ 42%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_pipeline.cpp.o
[ 57%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_swap_chain.cpp.o
[ 71%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_window.cpp.o
[ 85%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/main.cpp.o
[100%] Linking CXX executable Vulkan-Starter
[100%] Built target Vulkan-Starter
the last line says Built target Vulkan-Starter is what matters, that’s our binary!
now we can change into the build directory, if you’re not already there cd build, and launch our GLFW Vulkan Window!
./Vulkan-Starter

and with any luck you’ll have a fully functional GLFW window that doesn’t immediately exit, but also doesn’t do anything!
Unfortunately That Was the Easy Part⌗
Next we have to build out the entire Vulkan graphics pipeline and an API to interface with the Graphics Card.

- image credit to Brendan Galea - Vulkan Game Engine Tutorial 2
Directory Structure for the Vulkan Triangle⌗
You’ll need to download these 4 files if you haven’t grabbed them yet!
- lve_device.cpp & lve_device.hpp
- lve_swap_chain.cpp & lve_swap_chain.hpp
- if you watch the section of the video he talks about the edits you will need to make to those files! Basically it comes down to the namespace and the first few letters of the class name, and each method of the class.
and create 5 more:
glslc.shjde_pipeline.cppjde_pipeline.hppshaders/simple_shader.fragshaders/simple_shader.vert
my project directory tree AFTER video #7:
- the
.spvare the compiled output fromglslcwhich we’re getting to soon
├── CMakeLists.txt BUILDS PROJECT
└── src
├── first_app.cpp
├── first_app.hpp CLASS "FirstApp"
├── glslc.sh COMPILES SHADERS
├── jde_device.cpp
├── jde_device.hpp CLASS "JdeDevice"
├── jde_pipeline.cpp
├── jde_pipeline.hpp CLASS "JdePipeline"
├── jde_swap_chain.cpp
├── jde_swap_chain.hpp CLASS "JdeSwapChain"
├── jde_window.cpp
├── jde_window.hpp CLASS "JdeWindow"
├── main.cpp ENTRY POINT
└── shaders
├── simple_shader.frag SPIR-V FRAGMENT SHADER
├── simple_shader.frag.spv COMPILED SPIR-V FROM GLSLC
├── simple_shader.vert SPIR-V VERTEX SHADER
└── simple_shader.vert.spv COMPILED SPIR-V FROM GLSLC
One Nice Thing About the Shader Chain⌗
we can really only program two stages of the shader chain in Vulkan, the Vertex Shader and the Fragment Shader, which are right here:
src/shaders/simple_shader.frag⌗
is our fragment shader, responsible for determining the color of each pixel on the screen.
#version 450
layout (location =0) out vec4 outColor;
void main()
{
outColor = vec4(1.0, 1.0, 0.1, 1.0);
}
It defines a single output variable “outColor”, which is a 4-component vector representing the red, green, blue, and alpha (transparency) values of the pixel. In the main function, the outColor variable is set to a fixed value of (1.0, 1.0, 0.1, 1.0), which corresponds to a bright yellow color with full opacity.
src/shaders/simple_shader.vert⌗
is our vertex shader, which is responsible for determining the position of each vertex in the 3D space.
#version 450
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
It defines an array “positions” of 2D vectors, which represent the coordinates of three vertices. This shader will be used to draw the triangle on the screen using the provided vertex positions.
Shaders Are Done, Now Let’s Compile!⌗
and that’s the last time we’ll have to touch those before we get a Triangle on-screen, but that’s a ways off still. First we now need to utilize our VulkanSDK to run glslc. In the video he uses this script which works just fine in bash, so from fish I’ve just been launching a bash shell to run this src/glslc.sh file:
#!/bin/bash
$VULKAN_SDK/bin/glslc shaders/simple_shader.vert -o shaders/simple_shader.vert.spv
$VULKAN_SDK/bin/glslc shaders/simple_shader.frag -o shaders/simple_shader.frag.spv
Make sure that you change the mode of our file for executuion with chmod:
chmod +x src/glslc.sh
and finally
./src/glslc.sh
should run do the thing
It won’t tell you if you got it right, but glslc complain nicely if something didn’t go right.
if you’re using fish you may as well just go to BASH like I did:
bash ./src/glslc.sh
or
bash
./src/glslc.sh
to get a full shell environment up. If your environment variables aren’t working, make sure you’ve logged out of your user session since adding anything to your ~/.bashrc or ~/.bash_profile. If it really isn’t working just use the commands from the script as a guide to work through the process manually.
I didn’t go as far as incorporating the shader compilation step into the CMakeLists.txt file’s copy_assets function just yet, but that’s still something I will go on and do. I’ll update this post if/when it happens.
Toward The TRIANGLE!⌗
The GLFW window we wrote is about to get Vulkanized, and this page is going to start looking like GitHub!
As Brendan Stated in the tutorial, there’s a lot of extra boilerplate that gets in the way and does not need to be thought about at this stage. By following the tutorial beyond the point of this triangle example
There are 4 files to download from the tutorial and at very least the namespace will need to be changed to jde in those downloads. It’s not proper etiquate to link directly to those files, and you should watch those parts of the videos anyway to really see what to change in the files.
We’re jumping right to the Tringle BTW, these next couple of files are the only things we still need!
The expanded first_app.hpp⌗
#pragma once
#include "jde_window.hpp"
#include "jde_pipeline.hpp"
#include "jde_device.hpp"
#include "jde_swap_chain.hpp"
#include <memory>
#include <vector>
namespace jde {
class FirstApp {
public:
static constexpr int WIDTH = 800;
static constexpr int HEIGHT = 600;
FirstApp();
~FirstApp();
FirstApp(const FirstApp &) = delete;
FirstApp &operator=(const FirstApp &) = delete;
void run();
private:
void createPipelineLayout();
void createPipeline();
void createCommandBuffers();
void drawFrame();
JdeWindow jdeWindow{WIDTH, HEIGHT, "Hello Vulkan!"};
JdeDevice jdeDevice{jdeWindow};
JdeSwapChain jdeSwapChain{jdeDevice, jdeWindow.getExtent()};
std::unique_ptr<JdePipeline> jdePipeline;
VkPipelineLayout pipelineLayout;
std::vector<VkCommandBuffer> commandBuffers;
};
}
Our FirstApp class has a default constructor and destructor and a still similar run() method.
-
The
constructornow creates a pipeline layout, pipeline, and command buffers. -
The
destructornow destroys the pipeline layout. -
The
run()method is still our main loop in the application, it polls events and now calls thedrawFramemethod. -
The
createPipelineLayoutmethod creates a pipeline layout for the pipeline. It sets the layout count and push constant range count to 0 and creates the pipeline layout. -
The
createPipelinemethod creates a pipeline and assigns the render pass and pipeline layout to it. -
The
createCommandBuffersmethod resizes the command buffers vector to the size of the swap chain images. It allocates command buffers and begins recording them. It sets up a render pass, clears the color and depth values, and binds the pipeline and draws a triangle. -
The
drawFramemethod acquires an image from the swap chain, and begins and ends the command buffer while submitting to the mailbox queue.
and the accompanying translation unit is rather large:
first_app.cpp Just Got A Lot Bigger!⌗
#include "first_app.hpp"
#include <stdexcept>
#include <array>
namespace jde {
FirstApp::FirstApp()
{
createPipelineLayout();
createPipeline();
createCommandBuffers();
}
FirstApp::~FirstApp()
{
vkDestroyPipelineLayout(jdeDevice.device(), pipelineLayout, nullptr);
}
void FirstApp::run()
{
while (!jdeWindow.shouldClose())
{
glfwPollEvents();
drawFrame();
}
}
void FirstApp::createPipelineLayout()
{
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pSetLayouts = nullptr;
pipelineLayoutInfo.pushConstantRangeCount = 0;
pipelineLayoutInfo.pPushConstantRanges = nullptr;
if (vkCreatePipelineLayout(jdeDevice.device(), &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS)
{
throw std::runtime_error("failed to create pipeline layout!");
}
}
void FirstApp::createPipeline()
{
auto pipelineConfig = JdePipeline::defaultPipelineConfigInfo(jdeSwapChain.width(), jdeSwapChain.height());
pipelineConfig.renderPass = jdeSwapChain.getRenderPass();
pipelineConfig.pipelineLayout = pipelineLayout;
jdePipeline = std::make_unique<JdePipeline>(
jdeDevice,
"src/shaders/simple_shader.vert.spv",
"src/shaders/simple_shader.frag.spv",
pipelineConfig);
}
void FirstApp::createCommandBuffers()
{
commandBuffers.resize(jdeSwapChain.imageCount());
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandPool = jdeDevice.getCommandPool();
allocInfo.commandBufferCount = static_cast<uint32_t>(commandBuffers.size());
if(vkAllocateCommandBuffers(jdeDevice.device(), &allocInfo, commandBuffers.data())!= VK_SUCCESS)
{
throw std::runtime_error("failed to allocate command buffers!");
}
for (int i = 0; i < commandBuffers.size(); i++)
{
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS)
{
throw std::runtime_error("failed to begin recording command buffers!");
}
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = jdeSwapChain.getRenderPass();
renderPassInfo.framebuffer = jdeSwapChain.getFrameBuffer(i);
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = jdeSwapChain.getSwapChainExtent();
std::array<VkClearValue, 2> clearValues{};
clearValues[0].color = {0.2f, 0.5f, 0.9f, 1.0f};
clearValues[1].depthStencil = {1.0f, 0};
renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
renderPassInfo.pClearValues = clearValues.data();
vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
jdePipeline->bind(commandBuffers[i]);
vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffers[i]);
if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS)
{
throw std::runtime_error("failed to record command buffer");
}
}
}
void FirstApp::drawFrame()
{
uint32_t imageIndex;
auto result = jdeSwapChain.acquireNextImage(&imageIndex);
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
{
throw std::runtime_error("imageIndex failed to acquire swap chain image!");
}
result = jdeSwapChain.submitCommandBuffers(&commandBuffers[imageIndex], &imageIndex);
if (result != VK_SUCCESS)
{
throw std::runtime_error("result failed to acquire swap chain image!");
}
}
}
FirstApp constructor:
- creates a pipeline layout
- creates a pipeline
- creates command buffers
FirstApp destructor:
- destroys the pipeline layout
FirstApp::run():
- enters a loop that polls events and calls the
drawFramemethod as long as the window is not closed
FirstApp::createPipelineLayout():
- creates a pipeline layout with no set layouts or push constant ranges
FirstApp::createPipeline():
- creates a pipeline with the render pass and pipeline layout from the
JdeSwapChainandJdePipelineclasses, and assigns it to thejdePipelinemember variable
FirstApp::createCommandBuffers():
- resizes the command buffers vector to the number of images in the swap chain allocates command buffers and begins recording them
- sets up a render pass, clears the color and depth values, and binds the pipeline and draws a triangle
FirstApp::drawFrame():
acquires an image from the swap chain
begins and ends the command buffer and submits it to the mailbox queue
Vulkan Graphics Pipeline Configuration⌗
jde_pipeline.hpp⌗
#pragma once
#include "jde_device.hpp"
#include <string>
#include <vector>
namespace jde {
struct PipelineConfigInfo {
VkViewport viewport;
VkRect2D scissor;
VkPipelineViewportStateCreateInfo viewportInfo;
VkPipelineInputAssemblyStateCreateInfo inputAssemblyInfo;
VkPipelineRasterizationStateCreateInfo rasterizationInfo;
VkPipelineMultisampleStateCreateInfo multisampleInfo;
VkPipelineColorBlendAttachmentState colorBlendAttachment;
VkPipelineColorBlendStateCreateInfo colorBlendInfo;
VkPipelineDepthStencilStateCreateInfo depthStencilInfo;
VkPipelineLayout pipelineLayout = nullptr;
VkRenderPass renderPass = nullptr;
uint32_t subpass = 0;
};
class JdePipeline {
public:
JdePipeline(
JdeDevice &device,
const std::string& vertFilepath,
const std::string& fragFilepath,
const PipelineConfigInfo& configInfo);
~JdePipeline();
JdePipeline(const JdePipeline&) = delete;
void operator=(const JdePipeline&) = delete;
void bind(VkCommandBuffer commandBuffer);
static PipelineConfigInfo defaultPipelineConfigInfo(uint32_t width, uint32_t height);
private:
static std::vector<char> readFile(const std::string& filepath);
void createGraphicsPipeline(
const std::string& vertFilepath,
const std::string& fragFilepath,
const PipelineConfigInfo& configInfo);
void createShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule);
// implicitly will outlive any class that depends on it. "aggregation"
JdeDevice& jdeDevice;
VkPipeline graphicsPipeline;
VkShaderModule vertShaderModule;
VkShaderModule fragShaderModule;
};
}
This code is defining a class called JdePipeline in the jde namespace. The class has a constructor, destructor, and a function called bind().
-
The
constructortakes in aJdeDeviceobject, the file paths for avertex shaderandfragment shader, and aPipelineConfigInfostruct. -
The
destructorwill clean up the pipeline and shader modules. Thebind()function is used to bind the pipeline to a command buffer. -
The
JdePipelineclass also has a static function calleddefaultPipelineConfigInfo()that returns aPipelineConfigInfostruct with default values for its member variables. -
The class has a private function called
createGraphicsPipeline()that creates the pipeline using the provided configuration and shaders. -
It also has a private function called
createShaderModule()that creates a shader module from the provided code.
The class has a member variable graphicsPipeline of type VkPipeline, which will store the pipeline object.
-
It also has two member variables of type
VkShaderModule, which will store thevertex shaderandfragment shadermodules. -
The class also has a reference to the
JdeDeviceobject, which is passed in through theconstructorand is used to interact with the device throughout the lifetime of the pipeline.
and the massive list of groupings of definitions lie ahead:
jde_pipeline.cpp⌗
#include "jde_pipeline.hpp"
#include <fstream>
#include <stdexcept>
#include <iostream>
#include <cassert>
namespace jde {
JdePipeline::JdePipeline(
JdeDevice &device,
const std::string& vertFilepath,
const std::string& fragFilepath,
const PipelineConfigInfo& configInfo):
jdeDevice{device}
{
createGraphicsPipeline(vertFilepath, fragFilepath, configInfo);
}
JdePipeline::~JdePipeline()
{
vkDestroyShaderModule(jdeDevice.device(), vertShaderModule, nullptr);
vkDestroyShaderModule(jdeDevice.device(), fragShaderModule, nullptr);
vkDestroyPipeline(jdeDevice.device(), graphicsPipeline, nullptr);
}
std::vector<char> JdePipeline::readFile(const std::string& filepath)
{
std::ifstream file{filepath, std::ios::ate | std::ios::binary};
if (!file.is_open())
{
throw std::runtime_error("failed to open file: " + filepath);
}
size_t fileSize = static_cast<size_t>(file.tellg());
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), fileSize);
file.close();
return buffer;
}
void JdePipeline::createGraphicsPipeline(
const std::string& vertFilepath,
const std::string& fragFilepath,
const PipelineConfigInfo& configInfo)
{
assert(configInfo.pipelineLayout != VK_NULL_HANDLE &&
"Cannot create graphics pipeline:: no pipelineLayout provided in configInfo");
assert(configInfo.renderPass != VK_NULL_HANDLE &&
"Cannot create graphics pipeline:: no pipelineLayout provided in configInfo");
auto vertCode = readFile(vertFilepath);
auto fragCode = readFile(fragFilepath);
createShaderModule(vertCode, &vertShaderModule);
createShaderModule(fragCode, &fragShaderModule);
VkPipelineShaderStageCreateInfo shaderStages[2];
shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
shaderStages[0].module = vertShaderModule;
shaderStages[0].pName = "main";
shaderStages[0].flags = 0;
shaderStages[0].pNext = nullptr;
shaderStages[0].pSpecializationInfo = nullptr;
shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
shaderStages[1].module = fragShaderModule;
shaderStages[1].pName = "main";
shaderStages[1].flags = 0;
shaderStages[1].pNext = nullptr;
shaderStages[1].pSpecializationInfo = nullptr;
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
vertexInputInfo.pVertexBindingDescriptions = nullptr;
VkPipelineViewportStateCreateInfo viewportInfo{};
viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportInfo.viewportCount = 1;
viewportInfo.pViewports = &configInfo.viewport;
viewportInfo.scissorCount = 1;
viewportInfo.pScissors = &configInfo.scissor;
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &configInfo.inputAssemblyInfo;
pipelineInfo.pViewportState = &viewportInfo;
pipelineInfo.pRasterizationState = &configInfo.rasterizationInfo;
pipelineInfo.pMultisampleState = &configInfo.multisampleInfo;
pipelineInfo.pColorBlendState = &configInfo.colorBlendInfo;
pipelineInfo.pDepthStencilState = &configInfo.depthStencilInfo;
pipelineInfo.pDynamicState = nullptr;
pipelineInfo.layout = configInfo.pipelineLayout;
pipelineInfo.renderPass = configInfo.renderPass;
pipelineInfo.subpass = configInfo.subpass;
pipelineInfo.basePipelineIndex = -1;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
if (vkCreateGraphicsPipelines(
jdeDevice.device(),
VK_NULL_HANDLE,
1,
&pipelineInfo,
nullptr,
&graphicsPipeline) != VK_SUCCESS)
{
throw std::runtime_error("failed to create graphics pipeline");
}
}
void JdePipeline::createShaderModule(const std::vector<char>& code, VkShaderModule* shaderModule)
{
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
if (vkCreateShaderModule(jdeDevice.device(), &createInfo, nullptr, shaderModule) != VK_SUCCESS)
{
throw std::runtime_error("failed to create shader module");
}
}
void JdePipeline::bind(VkCommandBuffer commandBuffer)
{
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
}
PipelineConfigInfo JdePipeline::defaultPipelineConfigInfo(uint32_t width, uint32_t height)
{
PipelineConfigInfo configInfo{};
configInfo.inputAssemblyInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
configInfo.inputAssemblyInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
configInfo.inputAssemblyInfo.primitiveRestartEnable = VK_FALSE;
configInfo.viewport.x = 0.0f;
configInfo.viewport.y = 0.0f;
configInfo.viewport.width = static_cast<float>(width);
configInfo.viewport.height = static_cast<float>(height);
configInfo.viewport.minDepth = 0.0f;
configInfo.viewport.maxDepth = 1.0f;
configInfo.scissor.offset = {0, 0};
configInfo.scissor.extent = {width, height};
configInfo.rasterizationInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
configInfo.rasterizationInfo.depthClampEnable = VK_FALSE;
configInfo.rasterizationInfo.rasterizerDiscardEnable = VK_FALSE;
configInfo.rasterizationInfo.polygonMode = VK_POLYGON_MODE_FILL;
configInfo.rasterizationInfo.lineWidth = 1.0f;
configInfo.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
configInfo.rasterizationInfo.frontFace = VK_FRONT_FACE_CLOCKWISE;
configInfo.rasterizationInfo.depthBiasEnable = VK_FALSE;
configInfo.rasterizationInfo.depthBiasConstantFactor = 0.0f; // Optional
configInfo.rasterizationInfo.depthBiasClamp = 0.0f; // Optional
configInfo.rasterizationInfo.depthBiasSlopeFactor = 0.0f; // Optional
configInfo.multisampleInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
configInfo.multisampleInfo.sampleShadingEnable = VK_FALSE;
configInfo.multisampleInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
configInfo.multisampleInfo.minSampleShading = 1.0f; // Optional
configInfo.multisampleInfo.pSampleMask = nullptr; // Optional
configInfo.multisampleInfo.alphaToCoverageEnable = VK_FALSE; // Optional
configInfo.multisampleInfo.alphaToOneEnable = VK_FALSE; // Optional
configInfo.colorBlendAttachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT |
VK_COLOR_COMPONENT_A_BIT;
configInfo.colorBlendAttachment.blendEnable = VK_FALSE;
configInfo.colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
configInfo.colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
configInfo.colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional
configInfo.colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional
configInfo.colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional
configInfo.colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional
configInfo.colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
configInfo.colorBlendInfo.logicOpEnable = VK_FALSE;
configInfo.colorBlendInfo.logicOp = VK_LOGIC_OP_COPY; // Optional
configInfo.colorBlendInfo.attachmentCount = 1;
configInfo.colorBlendInfo.pAttachments = &configInfo.colorBlendAttachment;
configInfo.colorBlendInfo.blendConstants[0] = 0.0f; // Optional
configInfo.colorBlendInfo.blendConstants[1] = 0.0f; // Optional
configInfo.colorBlendInfo.blendConstants[2] = 0.0f; // Optional
configInfo.colorBlendInfo.blendConstants[3] = 0.0f; // Optional
configInfo.depthStencilInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
configInfo.depthStencilInfo.depthTestEnable = VK_TRUE;
configInfo.depthStencilInfo.depthWriteEnable = VK_TRUE;
configInfo.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS;
configInfo.depthStencilInfo.depthBoundsTestEnable = VK_FALSE;
configInfo.depthStencilInfo.minDepthBounds = 0.0f; // Optional
configInfo.depthStencilInfo.maxDepthBounds = 1.0f; // Optional
configInfo.depthStencilInfo.stencilTestEnable = VK_FALSE;
configInfo.depthStencilInfo.front = {}; // Optional
configInfo.depthStencilInfo.back = {}; // Optional
return configInfo;
}
}
The JdePipeline class provides an implementation for creating a graphics pipeline in a Vulkan application.
- The constructor takes in a
JdeDeviceand file paths forvertex shaderandfragment shader, as well as aPipelineConfigInfostruct containing information about the pipeline layout and render pass. - The class uses the provided file paths to create and load a vertex and fragment shader module.
- It then uses the provided
PipelineConfigInfoand the loaded shaders to create the graphics pipeline. - The
destructordestroys the shader modules and the pipeline. - The
readFilemethod reads a file and returns the contents as a vector of chars. - The
createShaderModulemethod creates a shader module from a vector of chars. - The
createGraphicsPipelinemethod sets up the pipeline, including specifying the shaders, vertex input, and viewport information. defaultPipelineConfigInfois a helper method that returns aPipelineConfigInfostruct with default values for its fields. The method takes in thewidthandheightof theviewport, and sets theviewportandscissorfields accordingly. It also sets default values for theviewportInfo,inputAssemblyInfo,rasterizationInfo,multisampleInfo,colorBlendAttachment,colorBlendInfo, anddepthStencilInfofields.- The pipeline layout and render pass fields are initialized to
nullptr, and thesubpassfield is initialized to0. createGraphicsPipelineis the method responsible for creating the graphics pipeline. It takes in the filepaths for thevertex shaderandfragment shaders, and aPipelineConfigInfostruct that contains the configuration for the pipeline. The method starts by reading thevertex shaderandfragment shadersfrom the filepaths, and creating shader modules from the code.- It then sets up the
shaderStagesarray with thevertex shaderandfragment shadermodules, and sets up thevertexInputInfo,viewportInfo, andrasterizationInfostructs with the appropriate values. ThemultisampleInfo,colorBlendAttachment,colorBlendInfo, anddepthStencilInfostructs are also set up with default values. - It then creates the graphics pipeline by calling
vkCreateGraphicsPipelineswith the appropriate parameters. It also binds the pipeline layout and render pass to the pipeline.
A New And Improved jde_window.hpp Window Class⌗
#pragma once
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <string>
namespace jde {
class JdeWindow {
public:
JdeWindow(int w, int h, std::string name);
~JdeWindow();
JdeWindow(const JdeWindow &) = delete;
JdeWindow &operator=(const JdeWindow &) = delete;
bool shouldClose() { return glfwWindowShouldClose(window); }
VkExtent2D getExtent() {
return {
static_cast<uint32_t>(width),
static_cast<uint32_t>(height)
};
}
void createWindowSurface(VkInstance instance, VkSurfaceKHR *surface);
private:
void initWindow();
const int width;
const int height;
std::string windowName;
GLFWwindow *window;
};
}
- We define a class called
JdeWindowwithin thejdenamespace. - The
JdeWindowclass has a constructor that takes in an integer width, an integer height and a string name, it creates a window usingGLFWlibrary with the providedwidth,heightandname. - The
destructordestroys the window, and a copy constructor and assignment operator that have beendeletedto prevent object copying. - The
shouldClose()public method returns a boolean indicating whether the window has been marked as closed by the user or not, it usesGLFWlibrary to check the status of the window. - The
getExtentpublic method returns the size of the window as aVkExtent2Dstruct, which is a struct that contains the width and height of an area. - There is also a method called
createWindowSurfacethat creates a window surface and assigns it to the provided pointer, it takes in aVkInstanceand a pointer to aVkSurfaceKHR. - The class also has a private method called
initWindow()which initializes the window usingGLFWlibrary. It has several private member variables including width, height, windowName and window which are used to store information about the window.
and the final piece to our triangular puzzle..!
jde_window.cpp⌗
#include "jde_window.hpp"
#include <stdexcept>
namespace jde {
JdeWindow::JdeWindow(int w, int h, std::string name):
width{w}, height{h}, windowName{name}
{
initWindow();
}
JdeWindow::~JdeWindow()
{
glfwDestroyWindow(window);
glfwTerminate();
}
void JdeWindow::initWindow()
{
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(width, height, windowName.c_str(), nullptr, nullptr);
}
void JdeWindow::createWindowSurface(VkInstance instance, VkSurfaceKHR *surface)
{
if (glfwCreateWindowSurface(instance, window, nullptr, surface) != VK_SUCCESS)
{
throw std::runtime_error("failed to create window surface");
}
}
}
- The
JdeWindowclass creates aGLFWwindow with a specifiedwidth,height, andname. TheinitWindow()method is called in theconstructorto initialize theGLFWlibrary and create the window with the givenwidth,height, andname. - It has a
destructorthat destroys the window and terminatesGLFW. - The
shouldClose()method returns a boolean indicating whether the window should be closed or not. - The
getExtent()method returns aVkExtent2Dstruct with the width and height of the window. - The
createWindowSurfacemethod creates aVkSurfaceKHRfor the window by callingglfwCreateWindowSurface, passing in theVkInstance, theGLFWwindow, and anullptrfor the allocation callbacks. If the return value is notVK_SUCCESS, it throws a runtime error “failed to create window surface.”
And Finally Repeat The Build Steps From Before⌗
Run the following CMake command if you’re not using something like Emacs or VS Code:
cmake -B ./build -S .
and you should see around the same output as:
-- The C compiler identification is GNU 12.2.1
-- The CXX compiler identification is GNU 12.2.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found Vulkan: /lib64/libvulkan.so (found version "1.3.216") found components: glslc glslangValidator
-- Configuring done
-- Generating done
-- Build files have been written to: /home/mac/Documents/Projects/C++/Vulkan/build
change into the build directory and issue a
makecommand:
cd build
make
or I found that you can run make from anywhere using the -C flag make -C build
in any case you should see something similar to the following output:
[ 0%] Built target copy_assets
[ 14%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/first_app.cpp.o
[ 28%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_device.cpp.o
[ 42%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_pipeline.cpp.o
[ 57%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_swap_chain.cpp.o
[ 71%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/jde_window.cpp.o
[ 85%] Building CXX object CMakeFiles/Vulkan-Starter.dir/src/main.cpp.o
[100%] Linking CXX executable Vulkan-Starter
[100%] Built target Vulkan-Starter
the last line says Built target Vulkan-Starter is what matters, that’s our binary!
LAUNCH THAT TRIANGLE!⌗
./build/Vulkan-Starter
