Deep Dive into the Code Architecture for REVISING IN THE FUTURE, for personal use.
This code is designed to encapsulate the Vulkan setup process into a manageable and reusable structure, following object-oriented programming principles. This organization makes it easier to read, maintain, and expand the code as your Vulkan application grows in complexity. Here's a step-by-step breakdown of the class HelloTriangleApplication
, its functions, and why each function exists.
The Class Structure
class HelloTriangleApplication {
public:
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
This is the main driver class for the Vulkan application. It encapsulates the entire lifecycle of your program:
-
initWindow()
: Initializes the GLFW window for displaying Vulkan's output. -
initVulkan()
: Sets up all Vulkan resources, such as the instance, physical device, logical device, and queues. -
mainLoop()
: The rendering loop, where you keep the application running and process user input. -
cleanup()
: Cleans up all resources when the application exits.
This structure ensures that each part of the program has a clear, focused purpose.
Why use functions to break things up?
-
Code clarity:
- Instead of dumping all Vulkan setup code in
main()
, breaking it into functions likeinitVulkan
andcleanup
makes it more readable and easier to understand.
- Instead of dumping all Vulkan setup code in
-
Maintainability:
- If you need to modify the Vulkan setup or rendering loop, you can do so in an isolated function without breaking the rest of the code.
-
Reusability:
- If you create another Vulkan project in the future, you can reuse some of these functions, like
createInstance
orcreateLogicalDevice
, with little modification.
- If you create another Vulkan project in the future, you can reuse some of these functions, like
Public Method: run()
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
The run
function is the entry point for the Vulkan application. It acts as the master controller, calling other functions in a logical sequence.
-
initWindow()
:- Sets up the GLFW window for Vulkan rendering.
- Ensures Vulkan can create a surface to draw on.
-
initVulkan()
:- Handles the complex Vulkan initialization process:
- Connect to Vulkan (
createInstance
). - Choose a GPU (
pickPhysicalDevice
). - Set up an interface to communicate with the GPU (
createLogicalDevice
).
- Connect to Vulkan (
- Handles the complex Vulkan initialization process:
-
mainLoop()
:- Keeps the application running until the user closes the window.
- Processes input and, eventually, issues rendering commands.
-
cleanup()
:- Frees up memory and destroys Vulkan objects to prevent memory leaks.
Private Member Functions
These functions handle specific tasks within the Vulkan lifecycle. Let’s break them down one by one, discussing why they exist, how they’re implemented, and how they fit into the overall flow.
1. initWindow()
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
Purpose:
This function initializes the GLFW windowing system and creates a window that Vulkan can use for rendering. Vulkan itself doesn’t handle window creation—it relies on external libraries like GLFW.
Key Steps:
-
glfwInit()
:- Initializes the GLFW library, which manages the window.
-
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API)
:- Tells GLFW not to create an OpenGL context, as Vulkan doesn’t need it.
-
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE)
:- Makes the window non-resizable to simplify Vulkan’s handling of swap chains.
-
glfwCreateWindow(...)
:- Creates the actual window.
Why is it a separate function?
- Window initialization is independent of Vulkan initialization.
- It keeps the
run()
function clean and focused.
2. initVulkan()
void initVulkan() {
createInstance();
pickPhysicalDevice();
createLogicalDevice();
}
Purpose:
This function orchestrates all the Vulkan setup steps. It calls helper functions (createInstance
, pickPhysicalDevice
, etc.) to initialize Vulkan in a modular way.
Why is it structured like this?
- Vulkan initialization is complex and involves multiple steps.
- Each step has a specific purpose:
-
createInstance
: Connects to Vulkan. -
pickPhysicalDevice
: Chooses a GPU. -
createLogicalDevice
: Sets up communication with the GPU.
-
3. createInstance()
void createInstance() {
if (enableValidationLayers && !checkValidationLayerSupport()) {
throw std::runtime_error("validation layers requested, but not available!");
}
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
std::vector<const char*> requiredExtensions;
for(uint32_t i = 0; i < glfwExtensionCount; i++) {
requiredExtensions.emplace_back(glfwExtensions[i]);
}
createInfo.enabledExtensionCount = static_cast<uint32_t>(requiredExtensions.size());
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
}
Purpose:
This function creates a Vulkan instance, which is the foundation for all Vulkan operations.
Key Details:
-
Validation Layers:
- Debugging tools to catch errors during development.
-
checkValidationLayerSupport()
ensures the requested validation layers are available.
-
VkApplicationInfo
:- Provides basic information about the app (name, version, etc.).
- Helps Vulkan optimize for specific apps.
-
Extensions:
- Extensions allow Vulkan to interface with the operating system for windowing.
Why is it a separate function?
Creating an instance is a distinct step in Vulkan’s setup process, so it deserves its own function for clarity and modularity.
4. pickPhysicalDevice()
void pickPhysicalDevice() {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
for (const auto& device : devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
if (physicalDevice == VK_NULL_HANDLE) {
throw std::runtime_error("failed to find a suitable GPU!");
}
}
Purpose:
This function selects a Vulkan-compatible GPU to use for rendering.
Key Details:
-
vkEnumeratePhysicalDevices
:- Retrieves a list of Vulkan-compatible GPUs.
-
isDeviceSuitable()
:- Checks if a GPU supports the required features (e.g., graphics queues).
Why is it a separate function?
GPU selection is a key part of Vulkan initialization, and isolating it makes the code easier to read and debug.
5. createLogicalDevice()
void createLogicalDevice() {
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value();
queueCreateInfo.queueCount = 1;
float queuePriority = 1.0f;
queueCreateInfo.pQueuePriorities = &queuePriority;
VkPhysicalDeviceFeatures deviceFeatures{};
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.pQueueCreateInfos = &queueCreateInfo;
createInfo.queueCreateInfoCount = 1;
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount = 0;
if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
throw std::runtime_error("failed to create logical device!");
}
vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
}
Purpose:
This function creates a logical device to communicate with the GPU.
Key Details:
-
Queues:
- GPUs support multiple queues for different tasks (graphics, compute, etc.).
- This function sets up a queue for graphics commands.
Why is it a separate function?
Logical device creation is distinct from physical device selection, so separating it improves modularity and readability.
Top comments (0)