Before creating anything, Vulkan requires you to know what hardware you have. vkEnumeratePhysicalDevices() lists the GPUs. vkGetPhysicalDeviceProperties() gives capabilities. vkGetPhysicalDeviceQueueFamilyProperties() tells you which queues exist. This query phase is mandatory — you choose your GPU based on what it supports, not what OpenGL assumed for you.
VkInstance (Demo 15) ← root, connects app to Vulkan runtime
└─ VkPhysicalDevice (Demo 15) ← READ ONLY, represents actual GPU hardware
└─ VkDevice (Demo 16) ← logical connection, used to create all other objects
├─ VkQueue (Demo 16) ← graphics + present queues
├─ VkCommandPool + VkCommandBuffer (Demo 16) ← record GPU commands
├─ VkSurface + VkSwapchain (Demo 17) ← window + image pool
│ └─ VkImageView + VkFramebuffer (Demo 17) ← render targets
├─ VkRenderPass (Demo 17) ← load/store rules for attachments
├─ VkShaderModule (Demo 18) ← SPIR-V bytecode (embedded arrays)
├─ VkPipeline (Demo 18) ← ALL state baked at creation
└─ VkSemaphore × 2 + VkFence (Demo 18) ← frame synchronisation
VkSurfaceKHR (Demo 15) ← created from GLFW windowcmake_minimum_required(VERSION 3.20)
project(D15_VulkanEnv)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Vulkan REQUIRED)
set(GLFW_DIR $ENV{GLFW_DIR})
set(GLM_DIR $ENV{GLM_DIR})
include_directories(
${GLFW_DIR}/include
${GLM_DIR}
${Vulkan_INCLUDE_DIRS}
)
add_executable(D15_VulkanEnv src/main.cpp)
target_link_libraries(D15_VulkanEnv
Vulkan::Vulkan
${GLFW_DIR}/lib-vc2022/glfw3.lib
)// ═══════════════════════════════════════════════════════════════════════════
// RR GRAPHICS LAB — DAY 4 · DEMO 15
// Vulkan Environment — Instance, Validation Layers, Physical Device Query
// By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
// PROJECT NAME: D15_VulkanEnv
// FOLDER: C:\Labs\D15_VulkanEnv\
//
// WHAT THIS DEMO TEACHES:
// - Why Vulkan exists: explicit control vs OpenGL's hidden driver overhead
// - VkInstance: the Vulkan library handle — created first, destroyed last
// - Validation layers: VK_LAYER_KHRONOS_validation catches every API misuse
// in debug builds; stripped in release. Your first safety net.
// - VkPhysicalDevice: enumerating GPUs — you SELECT one, not create one
// - Querying GPU properties: name, vendor, type, API version, limits
// - VkApplicationInfo + VkInstanceCreateInfo: every Vulkan struct needs sType
// - The difference between instance extensions and device extensions
//
// WHAT YOU WILL SEE:
// - Terminal: Vulkan API version loaded
// - All available validation layers listed
// - All physical devices found with properties
// - GPU name, driver version, device type, queue family count
// - A window opens (GLFW + VK_KHR_surface) to prove surface support
// - No triangle yet — this is environment proof and query only
//
// BUILDS ON: Day 1–3 OpenGL knowledge. This is the Vulkan equivalent of
// Demo 1 (window creation) but with 10× more explicit setup.
// KEY INSIGHT: Everything OpenGL's driver did silently, Vulkan makes you
// ask for explicitly — starting with the instance itself.
//
// CMakeLists.txt:
// find_package(Vulkan REQUIRED)
// target_link_libraries(... Vulkan::Vulkan glfw)
// ═══════════════════════════════════════════════════════════════════════════
#include <vulkan/vulkan.h>
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <stdexcept>
// ─────────────────────────────────────────────────────────────────────────────
// Validation layer name — this single layer provides comprehensive API checking
// including parameter validation, object lifetime, threading rules, etc.
// ─────────────────────────────────────────────────────────────────────────────
const std::vector<const char*> validationLayers = {
"VK_LAYER_KHRONOS_validation"
};
// Enable validation in debug, disable in release
#ifdef NDEBUG
const bool enableValidation = false;
#else
const bool enableValidation = true;
#endif
// ─────────────────────────────────────────────────────────────────────────────
// Check if the validation layer we want is actually available on this machine
// (Vulkan SDK must be installed for validation layers to be present)
// ─────────────────────────────────────────────────────────────────────────────
bool checkValidationLayerSupport() {
uint32_t layerCount = 0;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr); // first call: get count
std::vector<VkLayerProperties> available(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, available.data()); // second: fill
for (const char* name : validationLayers) {
bool found = false;
for (const auto& layer : available) {
if (strcmp(name, layer.layerName) == 0) { found = true; break; }
}
if (!found) return false;
}
return true;
}
// ─────────────────────────────────────────────────────────────────────────────
// Decode VkPhysicalDeviceType to a readable string
// ─────────────────────────────────────────────────────────────────────────────
const char* deviceTypeName(VkPhysicalDeviceType t) {
switch(t) {
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "Integrated GPU";
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "Discrete GPU";
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "Virtual GPU";
case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU";
default: return "Other";
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Decode Vulkan version integer (packed: major.minor.patch)
// ─────────────────────────────────────────────────────────────────────────────
std::string vkVersionString(uint32_t v) {
return std::to_string(VK_VERSION_MAJOR(v)) + "." +
std::to_string(VK_VERSION_MINOR(v)) + "." +
std::to_string(VK_VERSION_PATCH(v));
}
int main() {
std::cout << "\n========================================\n";
std::cout << " RR Graphics Lab — Demo 15: Vulkan Env\n";
std::cout << "========================================\n\n";
// ── STEP 1: GLFW Init + Vulkan Support Check ──────────────────────────────
glfwInit();
if (!glfwVulkanSupported()) {
std::cerr << "ERROR: GLFW reports Vulkan is not supported on this machine.\n";
std::cerr << " Check Vulkan SDK installation and GPU driver.\n";
return -1;
}
std::cout << "[GLFW] Vulkan supported: YES\n\n";
// ── STEP 2: List All Available Validation Layers ──────────────────────────
uint32_t layerCount = 0;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> layers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, layers.data());
std::cout << "[Validation Layers] " << layerCount << " available:\n";
for (const auto& l : layers)
std::cout << " " << l.layerName << " — " << l.description << "\n";
std::cout << "\n";
// Verify our required layer exists
bool validationOK = checkValidationLayerSupport();
std::cout << "[Validation] VK_LAYER_KHRONOS_validation present: "
<< (validationOK ? "YES" : "NO — install Vulkan SDK") << "\n\n";
// ── STEP 3: Get Required Extensions (from GLFW + optional debug) ──────────
// GLFW tells us which surface extensions it needs (e.g. VK_KHR_surface,
// VK_KHR_win32_surface on Windows). We must pass these to VkInstanceCreateInfo.
uint32_t glfwExtCount = 0;
const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount);
std::vector<const char*> extensions(glfwExts, glfwExts + glfwExtCount);
// Add debug utils extension when validation is on
if (enableValidation) extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
std::cout << "[Extensions] Requesting " << extensions.size() << " instance extensions:\n";
for (const auto& e : extensions) std::cout << " " << e << "\n";
std::cout << "\n";
// ── STEP 4: CREATE VkInstance ─────────────────────────────────────────────
// VkApplicationInfo: metadata about your application
// sType MUST be set — Vulkan uses it to know which struct this is
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "RR Graphics Lab — D15";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "RR Lab Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_3; // request Vulkan 1.3
// VkInstanceCreateInfo: what we actually pass to vkCreateInstance
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)extensions.size();
createInfo.ppEnabledExtensionNames = extensions.data();
// Enable validation layers in debug mode
if (enableValidation && validationOK) {
createInfo.enabledLayerCount = (uint32_t)validationLayers.size();
createInfo.ppEnabledLayerNames = validationLayers.data();
std::cout << "[Instance] Validation layers ENABLED\n";
} else {
createInfo.enabledLayerCount = 0;
std::cout << "[Instance] Validation layers disabled (release build or SDK missing)\n";
}
VkInstance instance;
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
// vkCreateInstance returns VkResult — ALWAYS check it
// VK_SUCCESS = 0. Any non-zero value is an error.
if (result != VK_SUCCESS) {
std::cerr << "ERROR: vkCreateInstance failed (VkResult = " << result << ")\n";
return -1;
}
std::cout << "[Instance] VkInstance created successfully: " << instance << "\n\n";
// ── STEP 5: ENUMERATE PHYSICAL DEVICES ───────────────────────────────────
// Physical devices are the actual GPUs in the system.
// You enumerate them and SELECT the one you want — you do not create them.
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
if (deviceCount == 0) {
std::cerr << "ERROR: No Vulkan-capable GPUs found.\n";
vkDestroyInstance(instance, nullptr);
return -1;
}
std::vector<VkPhysicalDevice> physDevices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, physDevices.data());
std::cout << "[Physical Devices] " << deviceCount << " Vulkan-capable GPU(s) found:\n";
std::cout << std::string(60, '-') << "\n";
for (uint32_t i = 0; i < deviceCount; i++) {
VkPhysicalDeviceProperties props{};
vkGetPhysicalDeviceProperties(physDevices[i], &props);
VkPhysicalDeviceMemoryProperties memProps{};
vkGetPhysicalDeviceMemoryProperties(physDevices[i], &memProps);
std::cout << "GPU " << i << ": " << props.deviceName << "\n";
std::cout << " Type: " << deviceTypeName(props.deviceType) << "\n";
std::cout << " API version: " << vkVersionString(props.apiVersion) << "\n";
std::cout << " Driver ver: " << vkVersionString(props.driverVersion) << "\n";
std::cout << " Vendor ID: 0x" << std::hex << props.vendorID << std::dec << "\n";
std::cout << " Max image: " << props.limits.maxImageDimension2D << " px\n";
std::cout << " Max UBO: " << props.limits.maxUniformBufferRange / 1024 << " KB\n";
// Queue families — each family supports a subset of operations
uint32_t qfCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physDevices[i], &qfCount, nullptr);
std::vector<VkQueueFamilyProperties> qfProps(qfCount);
vkGetPhysicalDeviceQueueFamilyProperties(physDevices[i], &qfCount, qfProps.data());
std::cout << " Queue families: " << qfCount << "\n";
for (uint32_t q = 0; q < qfCount; q++) {
std::cout << " [" << q << "] count=" << qfProps[q].queueCount << " flags=";
if (qfProps[q].queueFlags & VK_QUEUE_GRAPHICS_BIT) std::cout << "GRAPHICS ";
if (qfProps[q].queueFlags & VK_QUEUE_COMPUTE_BIT) std::cout << "COMPUTE ";
if (qfProps[q].queueFlags & VK_QUEUE_TRANSFER_BIT) std::cout << "TRANSFER ";
std::cout << "\n";
}
// Memory heaps
std::cout << " Memory heaps: " << memProps.memoryHeapCount << "\n";
for (uint32_t h = 0; h < memProps.memoryHeapCount; h++) {
uint64_t mb = memProps.memoryHeaps[h].size / (1024*1024);
bool deviceLocal = (memProps.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0;
std::cout << " [" << h << "] " << mb << " MB"
<< (deviceLocal ? " (device-local / VRAM)" : " (system RAM)") << "\n";
}
std::cout << "\n";
}
// ── STEP 6: Create Window + Surface (proof of surface extension working) ──
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // No OpenGL context
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
GLFWwindow* window = glfwCreateWindow(800, 500,
"Demo 15 — Vulkan Environment (close to exit)", nullptr, nullptr);
VkSurfaceKHR surface;
result = glfwCreateWindowSurface(instance, window, nullptr, &surface);
if (result == VK_SUCCESS)
std::cout << "[Surface] VkSurfaceKHR created via GLFW — VK_KHR_surface working\n\n";
else
std::cout << "[Surface] WARNING: surface creation failed (VkResult=" << result << ")\n\n";
std::cout << "Window open. Close it to exit.\n";
while (!glfwWindowShouldClose(window)) glfwPollEvents();
// ── CLEANUP: Destroy in REVERSE order of creation ────────────────────────
// This is a Vulkan rule: child objects must be destroyed before parent objects
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr); // destroys everything instance-owned
glfwDestroyWindow(window);
glfwTerminate();
std::cout << "\n[Cleanup] All Vulkan objects destroyed. Done.\n";
return 0;
}
cd C:\\Labs\\D15_VulkanEnv cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release build\\Release\\D15_VulkanEnv.exe
- Intentionally break the sType field: In
VkApplicationInfo, changesType = VK_STRUCTURE_TYPE_APPLICATION_INFOtosType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO. Run. Validation layers immediately report a VUID error. This proves validation layers catch even subtle struct misuse. - Enumerate instance extensions: Before creating the instance, call
vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr)then fill a vector and print all available extensions. Compare what the SDK installed vs what GLFW needs. - Query device features: Call
vkGetPhysicalDeviceFeatures(physDevices[0], &features)and printfeatures.geometryShader,features.tessellationShader,features.samplerAnisotropy. These must be explicitly enabled in D16's VkDeviceCreateInfo before use.
A VkPhysicalDevice is read-only hardware facts. A VkDevice is your per-application interface to that hardware — the object you use to create everything else. In OpenGL the device was the implicit current context. In Vulkan you create it explicitly, choose which queue families to activate, and enable only the extensions you need. Once created, every subsequent Vulkan object (swapchain, pipeline, buffer) is created through the device.
cmake_minimum_required(VERSION 3.20)
project(D16_VulkanDevice)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Vulkan REQUIRED)
set(GLFW_DIR $ENV{GLFW_DIR})
set(GLM_DIR $ENV{GLM_DIR})
include_directories(
${GLFW_DIR}/include
${GLM_DIR}
${Vulkan_INCLUDE_DIRS}
)
add_executable(D16_VulkanDevice src/main.cpp)
target_link_libraries(D16_VulkanDevice
Vulkan::Vulkan
${GLFW_DIR}/lib-vc2022/glfw3.lib
)// ═══════════════════════════════════════════════════════════════════════════
// RR GRAPHICS LAB — DAY 4 · DEMO 16
// Vulkan Device — Logical Device, Queues, Command Pool, Command Buffer
// By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
// PROJECT NAME: D16_VulkanDevice
// FOLDER: C:\Labs\D16_VulkanDevice\
//
// WHAT THIS DEMO TEACHES:
// - VkPhysicalDevice → VkDevice: selecting the GPU and creating a logical
// interface to it (the device is your per-application GPU handle)
// - Queue family selection: finding a family that supports GRAPHICS
// AND a family that supports PRESENT (surface presentation)
// These may be the same family (usually) or different ones
// - VkDeviceQueueCreateInfo: how many queues + their priorities (0.0–1.0)
// - VkDevice creation: enabling device extensions (VK_KHR_swapchain)
// - VkQueue retrieval: vkGetDeviceQueue — not created, just obtained
// - VkCommandPool: allocates memory for command buffers
// - VkCommandBuffer: records GPU commands but does NOT execute them yet
// - The difference between recording and submitting commands
//
// WHAT YOU WILL SEE:
// - Terminal: selected GPU + queue family indices
// - Logical device created + validated
// - Command pool and command buffer allocated
// - A command buffer recorded with a begin/end (no real commands yet)
// - Window stays open to confirm everything without crashing
//
// BUILDS ON: Demo 15 (instance + surface)
// KEY INSIGHT: In OpenGL, the "device" was implicit — glBegin/glEnd just
// worked. In Vulkan you explicitly create your device, choose
// which queue to submit work to, and pre-record all commands
// before any GPU execution happens.
// ═══════════════════════════════════════════════════════════════════════════
#include <vulkan/vulkan.h>
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <optional>
#include <set>
#include <stdexcept>
#include <cstring>
const std::vector<const char*> validationLayers = { "VK_LAYER_KHRONOS_validation" };
const std::vector<const char*> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
// ─────────────────────────────────────────────────────────────────────────────
// Queue family indices — we need two:
// graphicsFamily: a queue that can execute draw commands
// presentFamily: a queue that can present images to the surface
// Both are usually the same index on desktop GPUs
// ─────────────────────────────────────────────────────────────────────────────
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() { return graphicsFamily.has_value() && presentFamily.has_value(); }
};
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device, VkSurfaceKHR surface) {
QueueFamilyIndices indices;
uint32_t count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, nullptr);
std::vector<VkQueueFamilyProperties> families(count);
vkGetPhysicalDeviceQueueFamilyProperties(device, &count, families.data());
for (uint32_t i = 0; i < count; i++) {
// Check for graphics support
if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
indices.graphicsFamily = i;
// Check for presentation support — queue must be able to present to our surface
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
if (presentSupport) indices.presentFamily = i;
if (indices.isComplete()) break; // found both — done
}
return indices;
}
// ─────────────────────────────────────────────────────────────────────────────
// Pick the best physical device (prefer discrete GPU over integrated)
// ─────────────────────────────────────────────────────────────────────────────
VkPhysicalDevice selectPhysicalDevice(VkInstance instance, VkSurfaceKHR surface) {
uint32_t count = 0;
vkEnumeratePhysicalDevices(instance, &count, nullptr);
std::vector<VkPhysicalDevice> devices(count);
vkEnumeratePhysicalDevices(instance, &count, devices.data());
VkPhysicalDevice best = VK_NULL_HANDLE;
int bestScore = -1;
for (auto& d : devices) {
VkPhysicalDeviceProperties p{};
vkGetPhysicalDeviceProperties(d, &p);
auto qf = findQueueFamilies(d, surface);
if (!qf.isComplete()) continue;
int score = (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) ? 1000 : 1;
score += p.limits.maxImageDimension2D;
if (score > bestScore) { bestScore = score; best = d; }
}
return best;
}
int main() {
std::cout << "\n========================================\n";
std::cout << " RR Graphics Lab — Demo 16: Vulkan Device\n";
std::cout << "========================================\n\n";
// ── Boilerplate from Demo 15 (instance + surface) ──────────────────────
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
GLFWwindow* window = glfwCreateWindow(800, 500,
"Demo 16 — Vulkan Device Setup", nullptr, nullptr);
// Instance
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.apiVersion = VK_API_VERSION_1_3;
uint32_t glfwExtCount = 0;
const char** glfwExts = glfwGetRequiredInstanceExtensions(&glfwExtCount);
std::vector<const char*> exts(glfwExts, glfwExts + glfwExtCount);
VkInstanceCreateInfo instInfo{};
instInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instInfo.pApplicationInfo = &appInfo;
instInfo.enabledExtensionCount = (uint32_t)exts.size();
instInfo.ppEnabledExtensionNames = exts.data();
instInfo.enabledLayerCount = (uint32_t)validationLayers.size();
instInfo.ppEnabledLayerNames = validationLayers.data();
VkInstance instance;
vkCreateInstance(&instInfo, nullptr, &instance);
std::cout << "[1] VkInstance created\n";
// Surface
VkSurfaceKHR surface;
glfwCreateWindowSurface(instance, window, nullptr, &surface);
std::cout << "[2] VkSurfaceKHR created\n";
// ── STEP 3: SELECT PHYSICAL DEVICE ───────────────────────────────────────
VkPhysicalDevice physDevice = selectPhysicalDevice(instance, surface);
if (physDevice == VK_NULL_HANDLE) {
std::cerr << "ERROR: No suitable GPU found\n"; return -1;
}
VkPhysicalDeviceProperties gpuProps{};
vkGetPhysicalDeviceProperties(physDevice, &gpuProps);
std::cout << "[3] Physical device selected: " << gpuProps.deviceName << "\n";
// ── STEP 4: FIND QUEUE FAMILY INDICES ────────────────────────────────────
QueueFamilyIndices qfi = findQueueFamilies(physDevice, surface);
std::cout << "[4] Queue families:\n";
std::cout << " Graphics family index: " << qfi.graphicsFamily.value() << "\n";
std::cout << " Present family index: " << qfi.presentFamily.value() << "\n";
bool sameFam = (qfi.graphicsFamily.value() == qfi.presentFamily.value());
std::cout << " Same family: " << (sameFam ? "YES (typical on desktop GPUs)" : "NO (need two separate queues)") << "\n\n";
// ── STEP 5: CREATE LOGICAL DEVICE ────────────────────────────────────────
// We need one VkDeviceQueueCreateInfo per UNIQUE queue family
// (using a set to deduplicate in case graphics==present)
std::set<uint32_t> uniqueFamilies = {
qfi.graphicsFamily.value(), qfi.presentFamily.value()
};
float queuePriority = 1.0f; // priority range 0.0–1.0 (1.0 = highest)
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
for (uint32_t family : uniqueFamilies) {
VkDeviceQueueCreateInfo qi{};
qi.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
qi.queueFamilyIndex = family;
qi.queueCount = 1; // one queue per family is almost always enough
qi.pQueuePriorities = &queuePriority;
queueCreateInfos.push_back(qi);
}
// Device features we want to enable (none for now — add as needed)
VkPhysicalDeviceFeatures deviceFeatures{};
// deviceFeatures.samplerAnisotropy = VK_TRUE; // would enable anisotropic filtering
// Logical device creation
VkDeviceCreateInfo deviceInfo{};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceInfo.queueCreateInfoCount = (uint32_t)queueCreateInfos.size();
deviceInfo.pQueueCreateInfos = queueCreateInfos.data();
deviceInfo.pEnabledFeatures = &deviceFeatures;
deviceInfo.enabledExtensionCount = (uint32_t)deviceExtensions.size();
deviceInfo.ppEnabledExtensionNames = deviceExtensions.data();
// Validation layers at device level (deprecated in newer Vulkan but safe to set)
deviceInfo.enabledLayerCount = (uint32_t)validationLayers.size();
deviceInfo.ppEnabledLayerNames = validationLayers.data();
VkDevice device;
VkResult result = vkCreateDevice(physDevice, &deviceInfo, nullptr, &device);
if (result != VK_SUCCESS) {
std::cerr << "ERROR: vkCreateDevice failed (" << result << ")\n"; return -1;
}
std::cout << "[5] VkDevice (logical device) created: " << device << "\n\n";
// ── STEP 6: RETRIEVE QUEUES ───────────────────────────────────────────────
// Queues are retrieved from the device — they are NOT created separately
// You just call vkGetDeviceQueue with the family index and queue index (0)
VkQueue graphicsQueue, presentQueue;
vkGetDeviceQueue(device, qfi.graphicsFamily.value(), 0, &graphicsQueue);
vkGetDeviceQueue(device, qfi.presentFamily.value(), 0, &presentQueue);
std::cout << "[6] Queues retrieved:\n";
std::cout << " Graphics queue: " << graphicsQueue << "\n";
std::cout << " Present queue: " << presentQueue << "\n\n";
// ── STEP 7: CREATE COMMAND POOL ──────────────────────────────────────────
// Command pool allocates memory for command buffers.
// Commands recorded into buffers — submitted to GPU queue later.
// RESET_COMMAND_BUFFER_BIT: allows individual command buffer reset
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = qfi.graphicsFamily.value();
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VkCommandPool commandPool;
vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool);
std::cout << "[7] VkCommandPool created: " << commandPool << "\n";
// ── STEP 8: ALLOCATE COMMAND BUFFER ──────────────────────────────────────
// Command buffers are allocated FROM the pool, not created independently
// PRIMARY: can be submitted to queue directly
// SECONDARY: called from primary buffers (for reuse)
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer;
vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);
std::cout << "[8] VkCommandBuffer allocated: " << commandBuffer << "\n\n";
// ── STEP 9: RECORD A COMMAND BUFFER (empty — just begin/end) ─────────────
// This demonstrates the recording API. Real commands (draw calls) go
// between vkBeginCommandBuffer and vkEndCommandBuffer.
// ONE_TIME_SUBMIT_BIT: hint that this buffer will be submitted once then reset
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commandBuffer, &beginInfo);
// ── Inside here: vkCmdDraw, vkCmdCopyBuffer, vkCmdBeginRenderPass, etc. ──
// (nothing recorded yet — just proving the API works)
vkEndCommandBuffer(commandBuffer);
std::cout << "[9] Command buffer recorded (empty begin/end) — no crash = API working\n\n";
std::cout << "Summary:\n";
std::cout << " VkInstance → VkPhysicalDevice (selected: " << gpuProps.deviceName << ")\n";
std::cout << " VkPhysicalDevice → VkDevice (logical interface)\n";
std::cout << " VkDevice → VkQueue (graphics + present)\n";
std::cout << " VkDevice → VkCommandPool → VkCommandBuffer\n";
std::cout << "\nWindow open. Close to exit.\n";
while (!glfwWindowShouldClose(window)) glfwPollEvents();
// ── CLEANUP (reverse order) ───────────────────────────────────────────────
// Command buffers freed when pool is destroyed — no need to free individually
vkDestroyCommandPool(device, commandPool, nullptr);
vkDestroyDevice(device, nullptr); // implicitly frees queues
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
std::cout << "[Cleanup] Done.\n";
return 0;
}
cd C:\\Labs\\D16_VulkanDevice cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release build\\Release\\D16_VulkanDevice.exe
- Intentionally use the wrong queue family index: Change
qfi.graphicsFamily.value()to a hard-coded99in VkDeviceQueueCreateInfo. Run. Validation layers report: VUID violation — queue family index out of range for this device. Shows that queue family indices are hardware-specific and must be queried. - Enable samplerAnisotropy: Set
deviceFeatures.samplerAnisotropy = VK_TRUEin VkDeviceCreateInfo. Run. Confirm no error. Now comment it out but try to use anisotropy in a sampler (you would need one) — validation layers would catch the mismatch. Features must be explicitly enabled before use. - Allocate multiple command buffers: Change
commandBufferCount = 1to3and declareVkCommandBuffer cmdBufs[3]. Run. Observe: a pool allocates efficiently. In Demo 17, we allocate one buffer per swapchain image (typically 3).
0.04, 0.08, 0.18) clearing every frame — first real Vulkan pixelsIn OpenGL, glfwSwapBuffers() hid: acquiring a free backbuffer, waiting for vsync, flipping front/back. In Vulkan these are three explicit steps every frame: vkAcquireNextImageKHR() gets an image index, your recorded commands render into it, vkQueuePresentKHR() flips it. The semaphores signal between these steps so the GPU never reads and writes the same image simultaneously.
cmake_minimum_required(VERSION 3.20)
project(D17_VulkanSwapchain)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Vulkan REQUIRED)
set(GLFW_DIR $ENV{GLFW_DIR})
set(GLM_DIR $ENV{GLM_DIR})
include_directories(
${GLFW_DIR}/include
${GLM_DIR}
${Vulkan_INCLUDE_DIRS}
)
add_executable(D17_VulkanSwapchain src/main.cpp)
target_link_libraries(D17_VulkanSwapchain
Vulkan::Vulkan
${GLFW_DIR}/lib-vc2022/glfw3.lib
)// ═══════════════════════════════════════════════════════════════════════════
// RR GRAPHICS LAB — DAY 4 · DEMO 17
// Vulkan Swapchain — Surface Format, Present Mode, Render Pass, Framebuffers
// By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
// PROJECT NAME: D17_VulkanSwapchain
// FOLDER: C:\Labs\D17_VulkanSwapchain\
//
// WHAT THIS DEMO TEACHES:
// - VkSwapchainKHR: the queue of images presented to the screen
// — Vulkan equivalent of GLFW's double buffer, but fully explicit
// - Surface capabilities: querying min/max image count, extent limits
// - Surface format selection: prefer VK_FORMAT_B8G8R8A8_SRGB +
// VK_COLOR_SPACE_SRGB_NONLINEAR_KHR (standard sRGB display)
// - Present mode selection: prefer MAILBOX (triple-buffer, no tearing,
// low latency) over FIFO (V-sync) — FIFO is the only guaranteed mode
// - VkImageView: how Vulkan accesses a swapchain image (not directly)
// - VkRenderPass: describes the attachments (colour, depth) and what
// happens to them at the start and end of a render pass
// - VkFramebuffer: binds specific VkImageViews to a VkRenderPass attachment
// - Window shows a cleared navy screen — first pixels rendered by Vulkan
//
// WHAT YOU WILL SEE:
// - Terminal: surface format + present mode chosen with reasoning
// - Swapchain created with N images
// - Render pass and framebuffers created
// - Window cleared to navy each frame (vkCmdClearColorImage)
// - WASD: deliberate no-op (showing the render loop is running)
// - ESC: close
//
// BUILDS ON: Demo 16 (device + command buffer)
// ═══════════════════════════════════════════════════════════════════════════
#include <vulkan/vulkan.h>
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <optional>
#include <set>
#include <algorithm>
#include <limits>
#include <stdexcept>
#include <cstring>
#include <array>
const int WIN_W = 900, WIN_H = 600;
const int MAX_FRAMES_IN_FLIGHT = 2;
const std::vector<const char*> validationLayers = { "VK_LAYER_KHRONOS_validation" };
const std::vector<const char*> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() { return graphicsFamily.has_value() && presentFamily.has_value(); }
};
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities{};
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
// ── Helpers ──────────────────────────────────────────────────────────────────
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice d, VkSurfaceKHR s) {
QueueFamilyIndices idx;
uint32_t c=0; vkGetPhysicalDeviceQueueFamilyProperties(d,&c,nullptr);
std::vector<VkQueueFamilyProperties> f(c);
vkGetPhysicalDeviceQueueFamilyProperties(d,&c,f.data());
for(uint32_t i=0;i<c;i++){
if(f[i].queueFlags&VK_QUEUE_GRAPHICS_BIT) idx.graphicsFamily=i;
VkBool32 p=false; vkGetPhysicalDeviceSurfaceSupportKHR(d,i,s,&p);
if(p) idx.presentFamily=i;
if(idx.isComplete()) break;
}
return idx;
}
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice d, VkSurfaceKHR s) {
SwapChainSupportDetails det;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(d, s, &det.capabilities);
uint32_t fc=0; vkGetPhysicalDeviceSurfaceFormatsKHR(d,s,&fc,nullptr);
det.formats.resize(fc); vkGetPhysicalDeviceSurfaceFormatsKHR(d,s,&fc,det.formats.data());
uint32_t pc=0; vkGetPhysicalDeviceSurfacePresentModesKHR(d,s,&pc,nullptr);
det.presentModes.resize(pc); vkGetPhysicalDeviceSurfacePresentModesKHR(d,s,&pc,det.presentModes.data());
return det;
}
// ── Surface format selection ──────────────────────────────────────────────────
// Prefer 8-bit sRGB colour with standard colour space
// sRGB gives correct gamma — colours won't look washed out
VkSurfaceFormatKHR chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& formats) {
for (const auto& f : formats) {
if (f.format == VK_FORMAT_B8G8R8A8_SRGB &&
f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
std::cout << " Format: VK_FORMAT_B8G8R8A8_SRGB (preferred)\n";
return f;
}
}
std::cout << " Format: fallback to first available\n";
return formats[0];
}
// ── Present mode selection ──────────────────────────────────────────────────
// MAILBOX: triple-buffering, no tearing, low latency — best for games
// IMMEDIATE: no sync, possible tearing, lowest latency
// FIFO: V-sync equivalent — guaranteed to exist, used as fallback
VkPresentModeKHR choosePresentMode(const std::vector<VkPresentModeKHR>& modes) {
for (const auto& m : modes) {
if (m == VK_PRESENT_MODE_MAILBOX_KHR) {
std::cout << " Present mode: MAILBOX (triple-buffer, no tearing)\n";
return m;
}
}
std::cout << " Present mode: FIFO (V-sync fallback)\n";
return VK_PRESENT_MODE_FIFO_KHR;
}
// ── Swap extent (image resolution) ───────────────────────────────────────────
// Usually equals window size, but some monitors (HiDPI) need querying via GLFW
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& cap, GLFWwindow* w) {
if (cap.currentExtent.width != std::numeric_limits<uint32_t>::max())
return cap.currentExtent;
int W,H; glfwGetFramebufferSize(w,&W,&H);
VkExtent2D ext = {(uint32_t)W,(uint32_t)H};
ext.width = std::clamp(ext.width, cap.minImageExtent.width, cap.maxImageExtent.width);
ext.height = std::clamp(ext.height, cap.minImageExtent.height, cap.maxImageExtent.height);
return ext;
}
int main() {
std::cout << "\n========================================\n";
std::cout << " RR Graphics Lab — Demo 17: Swapchain\n";
std::cout << "========================================\n\n";
// ── Init (condensed from Demos 15+16) ─────────────────────────────────
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
GLFWwindow* window = glfwCreateWindow(WIN_W, WIN_H,
"Demo 17 — Vulkan Swapchain (ESC to quit)", nullptr, nullptr);
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.apiVersion = VK_API_VERSION_1_3;
uint32_t ec=0; const char** eg=glfwGetRequiredInstanceExtensions(&ec);
std::vector<const char*> exts(eg,eg+ec);
VkInstanceCreateInfo instInfo{};
instInfo.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instInfo.pApplicationInfo=&appInfo;
instInfo.enabledExtensionCount=(uint32_t)exts.size();
instInfo.ppEnabledExtensionNames=exts.data();
instInfo.enabledLayerCount=(uint32_t)validationLayers.size();
instInfo.ppEnabledLayerNames=validationLayers.data();
VkInstance instance; vkCreateInstance(&instInfo,nullptr,&instance);
VkSurfaceKHR surface; glfwCreateWindowSurface(instance,window,nullptr,&surface);
uint32_t dc=0; vkEnumeratePhysicalDevices(instance,&dc,nullptr);
std::vector<VkPhysicalDevice> pds(dc); vkEnumeratePhysicalDevices(instance,&dc,pds.data());
VkPhysicalDevice physDev = pds[0];
VkPhysicalDeviceProperties gpuProps{};
vkGetPhysicalDeviceProperties(physDev, &gpuProps);
std::cout << "[1] GPU: " << gpuProps.deviceName << "\n";
QueueFamilyIndices qfi = findQueueFamilies(physDev, surface);
std::set<uint32_t> uniqueFam = {qfi.graphicsFamily.value(), qfi.presentFamily.value()};
float prio = 1.0f;
std::vector<VkDeviceQueueCreateInfo> qcis;
for (uint32_t f : uniqueFam) {
VkDeviceQueueCreateInfo q{};
q.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
q.queueFamilyIndex=f; q.queueCount=1; q.pQueuePriorities=&prio;
qcis.push_back(q);
}
VkPhysicalDeviceFeatures df{};
VkDeviceCreateInfo dci{};
dci.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
dci.queueCreateInfoCount=(uint32_t)qcis.size(); dci.pQueueCreateInfos=qcis.data();
dci.pEnabledFeatures=&df;
dci.enabledExtensionCount=(uint32_t)deviceExtensions.size();
dci.ppEnabledExtensionNames=deviceExtensions.data();
dci.enabledLayerCount=(uint32_t)validationLayers.size();
dci.ppEnabledLayerNames=validationLayers.data();
VkDevice device; vkCreateDevice(physDev,&dci,nullptr,&device);
VkQueue graphicsQueue,presentQueue;
vkGetDeviceQueue(device,qfi.graphicsFamily.value(),0,&graphicsQueue);
vkGetDeviceQueue(device,qfi.presentFamily.value(), 0,&presentQueue);
std::cout << "[2] VkDevice + queues ready\n";
// ── STEP 3: QUERY SWAPCHAIN SUPPORT ──────────────────────────────────────
SwapChainSupportDetails swapDetails = querySwapChainSupport(physDev, surface);
std::cout << "[3] Swapchain support:\n";
std::cout << " Image count range: " << swapDetails.capabilities.minImageCount
<< " – " << swapDetails.capabilities.maxImageCount << "\n";
std::cout << " Surface formats available: " << swapDetails.formats.size() << "\n";
std::cout << " Present modes available: " << swapDetails.presentModes.size() << "\n";
// ── STEP 4: CHOOSE FORMAT, PRESENT MODE, EXTENT ───────────────────────────
VkSurfaceFormatKHR surfFmt = chooseSurfaceFormat(swapDetails.formats);
VkPresentModeKHR presMode = choosePresentMode(swapDetails.presentModes);
VkExtent2D extent = chooseSwapExtent(swapDetails.capabilities, window);
std::cout << " Extent: " << extent.width << "×" << extent.height << "\n\n";
// ── STEP 5: CREATE SWAPCHAIN ──────────────────────────────────────────────
// Request one extra image beyond minimum to avoid waiting for driver
uint32_t imageCount = swapDetails.capabilities.minImageCount + 1;
if (swapDetails.capabilities.maxImageCount > 0)
imageCount = std::min(imageCount, swapDetails.capabilities.maxImageCount);
VkSwapchainCreateInfoKHR sci{};
sci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
sci.surface = surface;
sci.minImageCount = imageCount;
sci.imageFormat = surfFmt.format;
sci.imageColorSpace = surfFmt.colorSpace;
sci.imageExtent = extent;
sci.imageArrayLayers = 1; // 1 = no stereoscopic 3D
sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
// If graphics and present are different queue families, images need CONCURRENT access
uint32_t qfis[] = {qfi.graphicsFamily.value(), qfi.presentFamily.value()};
if (qfi.graphicsFamily != qfi.presentFamily) {
sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
sci.queueFamilyIndexCount = 2;
sci.pQueueFamilyIndices = qfis;
} else {
sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; // faster
}
sci.preTransform = swapDetails.capabilities.currentTransform; // no rotation
sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; // no window transparency
sci.presentMode = presMode;
sci.clipped = VK_TRUE; // don't render pixels hidden by other windows
sci.oldSwapchain = VK_NULL_HANDLE;
VkSwapchainKHR swapchain;
if (vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain) != VK_SUCCESS) {
std::cerr << "ERROR: vkCreateSwapchainKHR failed\n"; return -1;
}
// Retrieve swapchain images (Vulkan allocates them — we just get handles)
uint32_t swapImgCount = 0;
vkGetSwapchainImagesKHR(device, swapchain, &swapImgCount, nullptr);
std::vector<VkImage> swapImages(swapImgCount);
vkGetSwapchainImagesKHR(device, swapchain, &swapImgCount, swapImages.data());
std::cout << "[4] VkSwapchainKHR created: " << swapImgCount << " images ("
<< extent.width << "×" << extent.height << ")\n";
// ── STEP 6: CREATE IMAGE VIEWS ────────────────────────────────────────────
// You never access a VkImage directly — you use a VkImageView
// The view describes: which part of the image, how to interpret it (2D colour, etc.)
std::vector<VkImageView> swapImageViews(swapImgCount);
for (uint32_t i = 0; i < swapImgCount; i++) {
VkImageViewCreateInfo ivci{};
ivci.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
ivci.image = swapImages[i];
ivci.viewType = VK_IMAGE_VIEW_TYPE_2D;
ivci.format = surfFmt.format;
ivci.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
ivci.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
ivci.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
ivci.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
ivci.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
ivci.subresourceRange.baseMipLevel = 0;
ivci.subresourceRange.levelCount = 1;
ivci.subresourceRange.baseArrayLayer = 0;
ivci.subresourceRange.layerCount = 1;
vkCreateImageView(device, &ivci, nullptr, &swapImageViews[i]);
}
std::cout << "[5] " << swapImgCount << " VkImageViews created\n";
// ── STEP 7: CREATE RENDER PASS ────────────────────────────────────────────
// A render pass describes the attachments used during rendering and how to
// treat them. LOAD_OP_CLEAR: clear at start. STORE_OP_STORE: keep result.
// The layout transition (UNDEFINED → PRESENT_SRC) tells Vulkan to prepare
// the image for presentation after rendering.
VkAttachmentDescription colourAttach{};
colourAttach.format = surfFmt.format;
colourAttach.samples = VK_SAMPLE_COUNT_1_BIT;
colourAttach.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // clear at start
colourAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE; // keep result
colourAttach.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colourAttach.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colourAttach.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // don't care what's there before
colourAttach.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // ready to present after
VkAttachmentReference colourRef{};
colourRef.attachment = 0; // index into pAttachments array
colourRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colourRef;
// Subpass dependency: ensures the render pass waits for the swapchain image
// to be ready before writing colour output
VkSubpassDependency dep{};
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
dep.dstSubpass = 0;
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.srcAccessMask = 0;
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpci{};
rpci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpci.attachmentCount = 1;
rpci.pAttachments = &colourAttach;
rpci.subpassCount = 1;
rpci.pSubpasses = &subpass;
rpci.dependencyCount = 1;
rpci.pDependencies = &dep;
VkRenderPass renderPass;
vkCreateRenderPass(device, &rpci, nullptr, &renderPass);
std::cout << "[6] VkRenderPass created\n";
// ── STEP 8: CREATE FRAMEBUFFERS ───────────────────────────────────────────
// One framebuffer per swapchain image — binds the image view to the render pass
std::vector<VkFramebuffer> framebuffers(swapImgCount);
for (uint32_t i = 0; i < swapImgCount; i++) {
VkFramebufferCreateInfo fbci{};
fbci.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbci.renderPass = renderPass;
fbci.attachmentCount = 1;
fbci.pAttachments = &swapImageViews[i];
fbci.width = extent.width;
fbci.height = extent.height;
fbci.layers = 1;
vkCreateFramebuffer(device, &fbci, nullptr, &framebuffers[i]);
}
std::cout << "[7] " << swapImgCount << " VkFramebuffers created\n\n";
// ── STEP 9: COMMAND POOL + BUFFERS ────────────────────────────────────────
VkCommandPoolCreateInfo cpi{};
cpi.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
cpi.queueFamilyIndex=qfi.graphicsFamily.value();
cpi.flags=VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VkCommandPool commandPool; vkCreateCommandPool(device,&cpi,nullptr,&commandPool);
std::vector<VkCommandBuffer> cmdBuffers(swapImgCount);
VkCommandBufferAllocateInfo cbai{};
cbai.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cbai.commandPool=commandPool;
cbai.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cbai.commandBufferCount=(uint32_t)swapImgCount;
vkAllocateCommandBuffers(device,&cbai,cmdBuffers.data());
// ── STEP 10: SYNC OBJECTS ──────────────────────────────────────────────────
// imageAvailable: GPU signals when swapchain image is ready to render into
// renderFinished: GPU signals when rendering is done, ready to present
// inFlightFence: CPU waits on this before starting a new frame
VkSemaphore imageAvailable, renderFinished;
VkFence inFlightFence;
VkSemaphoreCreateInfo semi{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; // start signaled so frame 0 doesn't deadlock
vkCreateSemaphore(device,&semi,nullptr,&imageAvailable);
vkCreateSemaphore(device,&semi,nullptr,&renderFinished);
vkCreateFence(device,&fci,nullptr,&inFlightFence);
std::cout << "[8] Sync objects: semaphores + fence ready\n";
std::cout << " Rendering navy clear colour each frame.\n";
std::cout << " ESC to quit.\n\n";
// ── RENDER LOOP ───────────────────────────────────────────────────────────
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
// Wait for previous frame to finish
vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFence);
// Acquire next swapchain image
uint32_t imgIdx;
vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
imageAvailable, VK_NULL_HANDLE, &imgIdx);
// Record command buffer — clear to navy
VkCommandBuffer cmd = cmdBuffers[imgIdx];
vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
vkBeginCommandBuffer(cmd, &bi);
// Begin render pass (this clears the attachment)
VkClearValue clearCol = {{{0.04f, 0.08f, 0.18f, 1.0f}}}; // navy blue
VkRenderPassBeginInfo rpbi{};
rpbi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpbi.renderPass = renderPass;
rpbi.framebuffer = framebuffers[imgIdx];
rpbi.renderArea = {{0,0}, extent};
rpbi.clearValueCount = 1;
rpbi.pClearValues = &clearCol;
vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
// (pipeline would go here in Demo 18)
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
// Submit
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo si{};
si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
si.waitSemaphoreCount = 1;
si.pWaitSemaphores = &imageAvailable;
si.pWaitDstStageMask = &waitStage;
si.commandBufferCount = 1;
si.pCommandBuffers = &cmd;
si.signalSemaphoreCount = 1;
si.pSignalSemaphores = &renderFinished;
vkQueueSubmit(graphicsQueue, 1, &si, inFlightFence);
// Present
VkPresentInfoKHR pi{};
pi.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
pi.waitSemaphoreCount = 1;
pi.pWaitSemaphores = &renderFinished;
pi.swapchainCount = 1;
pi.pSwapchains = &swapchain;
pi.pImageIndices = &imgIdx;
vkQueuePresentKHR(presentQueue, &pi);
}
vkDeviceWaitIdle(device);
// ── CLEANUP ───────────────────────────────────────────────────────────────
vkDestroyFence(device, inFlightFence, nullptr);
vkDestroySemaphore(device, renderFinished, nullptr);
vkDestroySemaphore(device, imageAvailable, nullptr);
vkDestroyCommandPool(device, commandPool, nullptr);
for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
vkDestroyRenderPass(device, renderPass, nullptr);
for (auto iv : swapImageViews) vkDestroyImageView(device, iv, nullptr);
vkDestroySwapchainKHR(device, swapchain, nullptr);
vkDestroyDevice(device, nullptr);
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window); glfwTerminate();
std::cout << "[Cleanup] Done.\n";
return 0;
}
cd C:\\Labs\\D17_VulkanSwapchain cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release build\\Release\\D17_VulkanSwapchain.exe
- Change the clear colour: In the render loop, change
clearCol = {{{0.04f, 0.08f, 0.18f, 1.0f}}}to orange{{{0.98f, 0.57f, 0.24f, 1.0f}}}. Rebuild. The window colour changes immediately. This is the Vulkan equivalent ofglClearColor()— but note that the colour is set per-frame in VkRenderPassBeginInfo, not as global state. - Force FIFO present mode: Change
choosePresentMode()to always returnVK_PRESENT_MODE_FIFO_KHR. Rebuild. Frame rate locks to vsync. Compare to MAILBOX — on a 144Hz display you should see MAILBOX running significantly faster (uncapped by CPU, only capped by vsync on present). - Remove the fence wait: Comment out
vkWaitForFences()at the start of the frame loop. Run. Validation layers report: command buffer in use. This shows why the fence exists — without it, the CPU races ahead and re-records a command buffer the GPU is still executing.
VkGraphicsPipelineCreateInfo on top of Demo 17's swapchain stack. Loads SPIR-V bytecode from vert.spv and frag.spv on disk (compiled from GLSL with glslc). The vertex shader hardcodes three positions using gl_VertexIndex; the fragment shader outputs solid white. Pipeline state covers vertex input (empty — no VkBuffer), input assembly, viewport, rasterizer, multisample, and colour blend. Calls vkCmdDraw(3, 1, 0, 0). Result: a white triangle on a navy background.This demo loads SPIR-V from disk. You must compile the shaders first with glslc (part of the Vulkan SDK, in %VULKAN_SDK%\Bin\), then copy the .spv files next to the .exe. If vert.spv or frag.spv is missing, the program prints a clear error message and exits cleanly.
Every OpenGL call like glEnable(GL_DEPTH_TEST), glPolygonMode(GL_FILL), glBlendFunc() changed global state the driver reconciled at every draw call. In Vulkan all of that is declared in VkGraphicsPipelineCreateInfo structs at creation time and compiled into one immutable object. At draw time: one vkCmdBindPipeline() — all state set, zero driver reconciliation.
REM In the project folder — same location as CMakeLists.txt REM 1. Create shader.vert and shader.frag (see source below) REM 2. Compile: glslc shader.vert -o vert.spv glslc shader.frag -o frag.spv REM 3. Copy vert.spv and frag.spv next to the .exe: REM build\Release\vert.spv REM build\Release\frag.spv
#version 450
// Positions hardcoded as a constant array — no vertex buffer needed.
// gl_VertexIndex is 0, 1, or 2 for the three vertices of the triangle.
// NDC coordinates: x and y both range from -1 (left/top) to +1 (right/bottom).
const vec2 positions[3] = vec2[3](
vec2( 0.0, -0.5), // top centre
vec2( 0.5, 0.5), // bottom right
vec2(-0.5, 0.5) // bottom left
);
void main() {
// gl_Position is the built-in output: clip-space position (x, y, z, w)
// z=0.0 (on the near plane), w=1.0 (no perspective divide needed)
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
#version 450
// layout(location=0): this output maps to colour attachment 0
// which is our swapchain image (bound via framebuffer)
layout(location = 0) out vec4 outColor;
void main() {
// Solid white: vec4(R, G, B, A) all 1.0
// Change this to vec4(0.98, 0.57, 0.24, 1.0) for tactical orange
outColor = vec4(1.0, 1.0, 1.0, 1.0);
}
cmake_minimum_required(VERSION 3.20)
project(D18_VulkanTriangle)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Vulkan REQUIRED)
set(GLFW_DIR $ENV{GLFW_DIR})
set(GLM_DIR $ENV{GLM_DIR})
include_directories(
${GLFW_DIR}/include
${GLM_DIR}
${Vulkan_INCLUDE_DIRS}
)
add_executable(D18_VulkanTriangle src/main.cpp)
target_link_libraries(D18_VulkanTriangle
Vulkan::Vulkan
${GLFW_DIR}/lib-vc2022/glfw3.lib
)// ═══════════════════════════════════════════════════════════════════════════
// RR GRAPHICS LAB — DAY 4 · DEMO 18
// Vulkan First Triangle — Full Graphics Pipeline + SPIR-V + vkCmdDraw
// By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
// PROJECT NAME: D18_VulkanTriangle
// FOLDER: C:\Labs\D18_VulkanTriangle\
//
// ── BEFORE BUILDING ────────────────────────────────────────────────────────
// Compile the GLSL shaders to SPIR-V first (in the project folder):
//
// glslc shader.vert -o vert.spv
// glslc shader.frag -o frag.spv
//
// Copy vert.spv and frag.spv into the same folder as the .exe before running.
// glslc is part of the Vulkan SDK — found in %VULKAN_SDK%\Bin\glslc.exe
// ───────────────────────────────────────────────────────────────────────────
//
// WHAT THIS DEMO TEACHES:
// - SPIR-V shaders: compile GLSL offline with glslc, load binary at runtime
// - VkShaderModule: wraps SPIR-V bytecode loaded from a .spv file
// - VkPipelineLayout: root signature — empty here (no descriptors yet)
// - VkGraphicsPipelineCreateInfo: all render state baked in one struct:
// vertex input (empty — positions hardcoded in vertex shader)
// input assembly (TRIANGLE_LIST)
// viewport + scissor (static)
// rasterizer (fill, no cull, clockwise front face)
// multisample (off)
// colour blend (opaque write, no blending)
// - vkCmdDraw(3, 1, 0, 0): draw 3 vertices, 1 instance — the triangle
// - Shader modules destroyed immediately after pipeline creation
// - Complete acquire -> record -> submit -> present frame loop
//
// WHAT YOU WILL SEE:
// - White triangle on a navy background
// - Vertex positions hardcoded in shader.vert via gl_VertexIndex
// - Fragment shader outputs vec4(1.0, 1.0, 1.0, 1.0) — solid white
// - No VkBuffer — that comes in Day 4 Lab 4
// - ESC to close
//
// BUILDS ON: Demo 17 (swapchain + render pass + framebuffers + frame loop)
//
// KEY INSIGHT: VkPipeline is immutable. All state you would change with
// glEnable() / glBlendFunc() / glUseProgram() is baked in at creation.
// Different state = different pipeline object.
// ═══════════════════════════════════════════════════════════════════════════
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <optional>
#include <set>
#include <algorithm>
#include <limits>
#include <fstream>
#include <stdexcept>
#include <cstring>
const int WIN_W = 900, WIN_H = 600;
const std::vector<const char*> validationLayers = { "VK_LAYER_KHRONOS_validation" };
const std::vector<const char*> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
// ─────────────────────────────────────────────────────────────────────────────
// readSpvFile — load a compiled SPIR-V binary (.spv) from disk
// Opens in binary mode (ios::binary), reads all bytes into a char vector.
// The vector is then cast to uint32_t* for VkShaderModuleCreateInfo.pCode.
// ─────────────────────────────────────────────────────────────────────────────
static std::vector<char> readSpvFile(const std::string& path) {
// ios::ate: open at end so tellg() gives file size immediately
std::ifstream file(path, std::ios::ate | std::ios::binary);
if (!file.is_open()) {
throw std::runtime_error(
"Cannot open SPIR-V file: " + path +
"\nRun: glslc shader.vert -o vert.spv AND glslc shader.frag -o frag.spv"
);
}
size_t fileSize = (size_t)file.tellg();
std::vector<char> buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), fileSize);
return buffer;
}
// ─────────────────────────────────────────────────────────────────────────────
// createShaderModule — wrap loaded SPIR-V bytes in a VkShaderModule
// ─────────────────────────────────────────────────────────────────────────────
static VkShaderModule createShaderModule(VkDevice device, const std::vector<char>& code) {
VkShaderModuleCreateInfo ci{};
ci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
ci.codeSize = code.size();
ci.pCode = reinterpret_cast<const uint32_t*>(code.data());
VkShaderModule module;
if (vkCreateShaderModule(device, &ci, nullptr, &module) != VK_SUCCESS)
throw std::runtime_error("vkCreateShaderModule failed");
return module;
}
// ── Queue family helpers ──────────────────────────────────────────────────────
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() { return graphicsFamily.has_value() && presentFamily.has_value(); }
};
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice dev, VkSurfaceKHR surf) {
QueueFamilyIndices idx;
uint32_t c = 0;
vkGetPhysicalDeviceQueueFamilyProperties(dev, &c, nullptr);
std::vector<VkQueueFamilyProperties> f(c);
vkGetPhysicalDeviceQueueFamilyProperties(dev, &c, f.data());
for (uint32_t i = 0; i < c; i++) {
if (f[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) idx.graphicsFamily = i;
VkBool32 p = false;
vkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surf, &p);
if (p) idx.presentFamily = i;
if (idx.isComplete()) break;
}
return idx;
}
VkSurfaceFormatKHR chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& formats) {
for (const auto& f : formats)
if (f.format == VK_FORMAT_B8G8R8A8_SRGB &&
f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) return f;
return formats[0];
}
VkPresentModeKHR choosePresentMode(const std::vector<VkPresentModeKHR>& modes) {
for (auto m : modes)
if (m == VK_PRESENT_MODE_MAILBOX_KHR) return m;
return VK_PRESENT_MODE_FIFO_KHR;
}
VkExtent2D chooseExtent(const VkSurfaceCapabilitiesKHR& caps, GLFWwindow* win) {
if (caps.currentExtent.width != UINT32_MAX) return caps.currentExtent;
int w, h;
glfwGetFramebufferSize(win, &w, &h);
VkExtent2D e = { (uint32_t)w, (uint32_t)h };
e.width = std::clamp(e.width, caps.minImageExtent.width, caps.maxImageExtent.width);
e.height = std::clamp(e.height, caps.minImageExtent.height, caps.maxImageExtent.height);
return e;
}
int main() {
std::cout << "\n===========================================\n";
std::cout << " RR Graphics Lab -- Demo 18: First Triangle\n";
std::cout << "===========================================\n\n";
std::cout << " Requires: vert.spv and frag.spv in same folder as .exe\n";
std::cout << " Compile: glslc shader.vert -o vert.spv\n";
std::cout << " glslc shader.frag -o frag.spv\n\n";
// ── GLFW ─────────────────────────────────────────────────────────────────
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
GLFWwindow* window = glfwCreateWindow(WIN_W, WIN_H,
"Demo 18 -- Vulkan First Triangle (ESC to quit)", nullptr, nullptr);
// ── INSTANCE ─────────────────────────────────────────────────────────────
VkApplicationInfo ai{};
ai.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
ai.apiVersion = VK_API_VERSION_1_0; // 1.0 for maximum compatibility
uint32_t ec = 0;
const char** eg = glfwGetRequiredInstanceExtensions(&ec);
std::vector<const char*> exts(eg, eg + ec);
VkInstanceCreateInfo ici{};
ici.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
ici.pApplicationInfo = &ai;
ici.enabledExtensionCount = (uint32_t)exts.size();
ici.ppEnabledExtensionNames = exts.data();
ici.enabledLayerCount = (uint32_t)validationLayers.size();
ici.ppEnabledLayerNames = validationLayers.data();
VkInstance instance;
if (vkCreateInstance(&ici, nullptr, &instance) != VK_SUCCESS) {
std::cerr << "ERROR: vkCreateInstance failed\n"; return -1;
}
// ── SURFACE ──────────────────────────────────────────────────────────────
VkSurfaceKHR surface;
glfwCreateWindowSurface(instance, window, nullptr, &surface);
// ── PHYSICAL DEVICE ──────────────────────────────────────────────────────
uint32_t dc = 0;
vkEnumeratePhysicalDevices(instance, &dc, nullptr);
std::vector<VkPhysicalDevice> pds(dc);
vkEnumeratePhysicalDevices(instance, &dc, pds.data());
VkPhysicalDevice physDev = VK_NULL_HANDLE;
for (auto& d : pds) {
VkPhysicalDeviceProperties p{};
vkGetPhysicalDeviceProperties(d, &p);
auto qf = findQueueFamilies(d, surface);
if (!qf.isComplete()) continue;
physDev = d;
if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) break;
}
if (physDev == VK_NULL_HANDLE) {
std::cerr << "ERROR: No suitable GPU\n"; return -1;
}
VkPhysicalDeviceProperties gpuProps{};
vkGetPhysicalDeviceProperties(physDev, &gpuProps);
std::cout << "[1] GPU: " << gpuProps.deviceName << "\n";
// ── LOGICAL DEVICE ────────────────────────────────────────────────────────
QueueFamilyIndices qfi = findQueueFamilies(physDev, surface);
std::set<uint32_t> uniqueFamilies = {
qfi.graphicsFamily.value(), qfi.presentFamily.value()
};
float prio = 1.0f;
std::vector<VkDeviceQueueCreateInfo> qcis;
for (uint32_t fam : uniqueFamilies) {
VkDeviceQueueCreateInfo q{};
q.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
q.queueFamilyIndex = fam;
q.queueCount = 1;
q.pQueuePriorities = &prio;
qcis.push_back(q);
}
VkPhysicalDeviceFeatures features{};
VkDeviceCreateInfo dci{};
dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
dci.queueCreateInfoCount = (uint32_t)qcis.size();
dci.pQueueCreateInfos = qcis.data();
dci.pEnabledFeatures = &features;
dci.enabledExtensionCount = (uint32_t)deviceExtensions.size();
dci.ppEnabledExtensionNames = deviceExtensions.data();
dci.enabledLayerCount = (uint32_t)validationLayers.size();
dci.ppEnabledLayerNames = validationLayers.data();
VkDevice device;
if (vkCreateDevice(physDev, &dci, nullptr, &device) != VK_SUCCESS) {
std::cerr << "ERROR: vkCreateDevice failed\n"; return -1;
}
VkQueue graphicsQueue, presentQueue;
vkGetDeviceQueue(device, qfi.graphicsFamily.value(), 0, &graphicsQueue);
vkGetDeviceQueue(device, qfi.presentFamily.value(), 0, &presentQueue);
// ── SWAPCHAIN ─────────────────────────────────────────────────────────────
VkSurfaceCapabilitiesKHR caps{};
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDev, surface, &caps);
uint32_t fmtN = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &fmtN, nullptr);
std::vector<VkSurfaceFormatKHR> fmts(fmtN);
vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &fmtN, fmts.data());
uint32_t pmN = 0;
vkGetPhysicalDeviceSurfacePresentModesKHR(physDev, surface, &pmN, nullptr);
std::vector<VkPresentModeKHR> pms(pmN);
vkGetPhysicalDeviceSurfacePresentModesKHR(physDev, surface, &pmN, pms.data());
VkSurfaceFormatKHR surfFmt = chooseSurfaceFormat(fmts);
VkPresentModeKHR presMode = choosePresentMode(pms);
VkExtent2D extent = chooseExtent(caps, window);
uint32_t imgCount = std::min(
caps.minImageCount + 1,
caps.maxImageCount > 0 ? caps.maxImageCount : UINT32_MAX);
VkSwapchainCreateInfoKHR sci{};
sci.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
sci.surface = surface;
sci.minImageCount = imgCount;
sci.imageFormat = surfFmt.format;
sci.imageColorSpace = surfFmt.colorSpace;
sci.imageExtent = extent;
sci.imageArrayLayers = 1;
sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
uint32_t qfArr[] = { qfi.graphicsFamily.value(), qfi.presentFamily.value() };
if (qfi.graphicsFamily != qfi.presentFamily) {
sci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
sci.queueFamilyIndexCount = 2;
sci.pQueueFamilyIndices = qfArr;
} else {
sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
}
sci.preTransform = caps.currentTransform;
sci.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
sci.presentMode = presMode;
sci.clipped = VK_TRUE;
VkSwapchainKHR swapchain;
if (vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain) != VK_SUCCESS) {
std::cerr << "ERROR: vkCreateSwapchainKHR failed\n"; return -1;
}
uint32_t siCount = 0;
vkGetSwapchainImagesKHR(device, swapchain, &siCount, nullptr);
std::vector<VkImage> swapImages(siCount);
vkGetSwapchainImagesKHR(device, swapchain, &siCount, swapImages.data());
std::vector<VkImageView> swapViews(siCount);
for (uint32_t i = 0; i < siCount; i++) {
VkImageViewCreateInfo ivci{};
ivci.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
ivci.image = swapImages[i];
ivci.viewType = VK_IMAGE_VIEW_TYPE_2D;
ivci.format = surfFmt.format;
ivci.components = {
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY
};
ivci.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
ivci.subresourceRange.baseMipLevel = 0;
ivci.subresourceRange.levelCount = 1;
ivci.subresourceRange.baseArrayLayer = 0;
ivci.subresourceRange.layerCount = 1;
vkCreateImageView(device, &ivci, nullptr, &swapViews[i]);
}
// ── RENDER PASS ───────────────────────────────────────────────────────────
VkAttachmentDescription colAtt{};
colAtt.format = surfFmt.format;
colAtt.samples = VK_SAMPLE_COUNT_1_BIT;
colAtt.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colAtt.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colAtt.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colAtt.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colAtt.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colRef{ 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colRef;
VkSubpassDependency dep{};
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
dep.dstSubpass = 0;
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.srcAccessMask = 0;
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rpci{};
rpci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rpci.attachmentCount = 1;
rpci.pAttachments = &colAtt;
rpci.subpassCount = 1;
rpci.pSubpasses = &subpass;
rpci.dependencyCount = 1;
rpci.pDependencies = &dep;
VkRenderPass renderPass;
vkCreateRenderPass(device, &rpci, nullptr, &renderPass);
// ── FRAMEBUFFERS ──────────────────────────────────────────────────────────
std::vector<VkFramebuffer> framebuffers(siCount);
for (uint32_t i = 0; i < siCount; i++) {
VkFramebufferCreateInfo fbci{};
fbci.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fbci.renderPass = renderPass;
fbci.attachmentCount = 1;
fbci.pAttachments = &swapViews[i];
fbci.width = extent.width;
fbci.height = extent.height;
fbci.layers = 1;
vkCreateFramebuffer(device, &fbci, nullptr, &framebuffers[i]);
}
std::cout << "[2] Swapchain + " << siCount
<< " images + views + framebuffers + renderpass ready\n";
// ── LOAD SPIR-V FROM DISK ─────────────────────────────────────────────────
std::cout << "[3] Building VkPipeline:\n";
std::cout << " Loading vert.spv and frag.spv...\n";
std::vector<char> vertCode, fragCode;
try {
vertCode = readSpvFile("vert.spv");
fragCode = readSpvFile("frag.spv");
} catch (const std::exception& e) {
std::cerr << "\nERROR: " << e.what() << "\n\n";
std::cerr << "To fix:\n";
std::cerr << " 1. Create shader.vert and shader.frag in the project folder\n";
std::cerr << " 2. glslc shader.vert -o vert.spv\n";
std::cerr << " 3. glslc shader.frag -o frag.spv\n";
std::cerr << " 4. Copy vert.spv + frag.spv next to .exe and rerun\n";
vkDestroyRenderPass(device, renderPass, nullptr);
for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
for (auto iv : swapViews) vkDestroyImageView(device, iv, nullptr);
vkDestroySwapchainKHR(device, swapchain, nullptr);
vkDestroyDevice(device, nullptr);
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window); glfwTerminate();
return -1;
}
std::cout << " vert.spv: " << vertCode.size() << " bytes | "
<< "frag.spv: " << fragCode.size() << " bytes\n";
VkShaderModule vertMod = createShaderModule(device, vertCode);
VkShaderModule fragMod = createShaderModule(device, fragCode);
std::cout << " Shader modules created\n";
// ── GRAPHICS PIPELINE ─────────────────────────────────────────────────────
VkPipelineShaderStageCreateInfo vertStage{};
vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertStage.module = vertMod;
vertStage.pName = "main";
VkPipelineShaderStageCreateInfo fragStage{};
fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragStage.module = fragMod;
fragStage.pName = "main";
VkPipelineShaderStageCreateInfo stages[] = { vertStage, fragStage };
// Vertex input: empty — positions hardcoded in shader.vert via gl_VertexIndex
VkPipelineVertexInputStateCreateInfo visc{};
visc.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
visc.vertexBindingDescriptionCount = 0;
visc.vertexAttributeDescriptionCount = 0;
VkPipelineInputAssemblyStateCreateInfo iasc{};
iasc.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
iasc.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
iasc.primitiveRestartEnable = VK_FALSE;
VkViewport viewport{};
viewport.x = 0.0f; viewport.y = 0.0f;
viewport.width = (float)extent.width; viewport.height = (float)extent.height;
viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f;
VkRect2D scissor{ {0,0}, extent };
VkPipelineViewportStateCreateInfo vpsc{};
vpsc.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
vpsc.viewportCount = 1; vpsc.pViewports = &viewport;
vpsc.scissorCount = 1; vpsc.pScissors = &scissor;
VkPipelineRasterizationStateCreateInfo rast{};
rast.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rast.depthClampEnable = VK_FALSE;
rast.rasterizerDiscardEnable = VK_FALSE;
rast.polygonMode = VK_POLYGON_MODE_FILL;
rast.lineWidth = 1.0f;
rast.cullMode = VK_CULL_MODE_NONE;
rast.frontFace = VK_FRONT_FACE_CLOCKWISE;
VkPipelineMultisampleStateCreateInfo mssc{};
mssc.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
mssc.sampleShadingEnable = VK_FALSE;
mssc.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState cba{};
cba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
cba.blendEnable = VK_FALSE;
VkPipelineColorBlendStateCreateInfo cbsc{};
cbsc.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
cbsc.logicOpEnable = VK_FALSE;
cbsc.attachmentCount = 1;
cbsc.pAttachments = &cba;
VkPipelineLayoutCreateInfo plci{};
plci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
VkPipelineLayout pipelineLayout;
vkCreatePipelineLayout(device, &plci, nullptr, &pipelineLayout);
VkGraphicsPipelineCreateInfo gpci{};
gpci.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
gpci.stageCount = 2;
gpci.pStages = stages;
gpci.pVertexInputState = &visc;
gpci.pInputAssemblyState = &iasc;
gpci.pViewportState = &vpsc;
gpci.pRasterizationState = &rast;
gpci.pMultisampleState = &mssc;
gpci.pDepthStencilState = nullptr;
gpci.pColorBlendState = &cbsc;
gpci.pDynamicState = nullptr;
gpci.layout = pipelineLayout;
gpci.renderPass = renderPass;
gpci.subpass = 0;
gpci.basePipelineHandle = VK_NULL_HANDLE;
VkPipeline pipeline;
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpci, nullptr, &pipeline)
!= VK_SUCCESS) {
std::cerr << "ERROR: vkCreateGraphicsPipelines failed\n"; return -1;
}
// Shader modules can be freed immediately after pipeline creation
vkDestroyShaderModule(device, fragMod, nullptr);
vkDestroyShaderModule(device, vertMod, nullptr);
std::cout << " VkPipeline created (shaders + rasterizer + blend + viewport)\n";
std::cout << " Shader modules destroyed (bytecode released)\n\n";
// ── COMMAND POOL + BUFFERS ────────────────────────────────────────────────
VkCommandPoolCreateInfo cpci{};
cpci.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
cpci.queueFamilyIndex = qfi.graphicsFamily.value();
cpci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VkCommandPool cmdPool;
vkCreateCommandPool(device, &cpci, nullptr, &cmdPool);
std::vector<VkCommandBuffer> cmdBufs(siCount);
VkCommandBufferAllocateInfo cbai{};
cbai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cbai.commandPool = cmdPool;
cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cbai.commandBufferCount = siCount;
vkAllocateCommandBuffers(device, &cbai, cmdBufs.data());
// ── SYNC OBJECTS ──────────────────────────────────────────────────────────
VkSemaphore imgAvail, renderDone;
VkFence inFlight;
VkSemaphoreCreateInfo semci{};
semci.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenci{};
fenci.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
vkCreateSemaphore(device, &semci, nullptr, &imgAvail);
vkCreateSemaphore(device, &semci, nullptr, &renderDone);
vkCreateFence(device, &fenci, nullptr, &inFlight);
std::cout << "[4] Ready. Rendering white triangle. ESC to quit.\n\n";
// ── RENDER LOOP ───────────────────────────────────────────────────────────
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
// Wait for previous frame to finish before re-recording its command buffer
vkWaitForFences(device, 1, &inFlight, VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlight);
uint32_t imgIdx;
VkResult res = vkAcquireNextImageKHR(
device, swapchain, UINT64_MAX, imgAvail, VK_NULL_HANDLE, &imgIdx);
if (res == VK_ERROR_OUT_OF_DATE_KHR) continue;
VkCommandBuffer cmd = cmdBufs[imgIdx];
vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo cbbi{};
cbbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &cbbi);
// Navy clear colour: rgb(10, 20, 46)
VkClearValue clearCol{};
clearCol.color.float32[0] = 0.04f;
clearCol.color.float32[1] = 0.08f;
clearCol.color.float32[2] = 0.18f;
clearCol.color.float32[3] = 1.00f;
VkRenderPassBeginInfo rpbi{};
rpbi.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpbi.renderPass = renderPass;
rpbi.framebuffer = framebuffers[imgIdx];
rpbi.renderArea = { {0, 0}, extent };
rpbi.clearValueCount = 1;
rpbi.pClearValues = &clearCol;
vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
// Draw 3 vertices (1 triangle). Vertex shader reads positions from
// gl_VertexIndex — no vertex buffer needed.
vkCmdDraw(cmd, 3, 1, 0, 0);
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo si{};
si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
si.waitSemaphoreCount = 1;
si.pWaitSemaphores = &imgAvail;
si.pWaitDstStageMask = &waitStage;
si.commandBufferCount = 1;
si.pCommandBuffers = &cmd;
si.signalSemaphoreCount = 1;
si.pSignalSemaphores = &renderDone;
vkQueueSubmit(graphicsQueue, 1, &si, inFlight);
VkPresentInfoKHR pi{};
pi.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
pi.waitSemaphoreCount = 1;
pi.pWaitSemaphores = &renderDone;
pi.swapchainCount = 1;
pi.pSwapchains = &swapchain;
pi.pImageIndices = &imgIdx;
vkQueuePresentKHR(presentQueue, &pi);
}
vkDeviceWaitIdle(device);
// ── CLEANUP — reverse creation order ─────────────────────────────────────
vkDestroyFence(device, inFlight, nullptr);
vkDestroySemaphore(device, renderDone, nullptr);
vkDestroySemaphore(device, imgAvail, nullptr);
vkDestroyCommandPool(device, cmdPool, nullptr);
vkDestroyPipeline(device, pipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
for (auto fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
vkDestroyRenderPass(device, renderPass, nullptr);
for (auto iv : swapViews) vkDestroyImageView(device, iv, nullptr);
vkDestroySwapchainKHR(device, swapchain, nullptr);
vkDestroyDevice(device, nullptr);
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
std::cout << "[Cleanup] All Vulkan objects destroyed. Day 4 complete.\n";
return 0;
}
cd C:\\Labs\\D18_VulkanTriangle cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release REM Copy compiled shaders next to the exe: copy vert.spv build\\Release\\vert.spv copy frag.spv build\\Release\\frag.spv build\\Release\\D18_VulkanTriangle.exe
- Change the triangle colour: In
shader.frag, changeoutColor = vec4(1.0, 1.0, 1.0, 1.0)tovec4(0.98, 0.57, 0.24, 1.0)(tactical orange). Recompile withglslc shader.frag -o frag.spv, copy to the build folder, rerun. No C++ rebuild needed — the pipeline reloads the .spv file at startup. - Move the triangle: In
shader.vert, change the threevec2position values. E.g. make it larger:vec2(0.0, -0.8),vec2(0.8, 0.8),vec2(-0.8, 0.8). Recompile and copy vert.spv. NDC coordinates go from -1 (left/top) to +1 (right/bottom). - Remove the fence wait: Comment out
vkWaitForFences()at the start of the frame loop. Run. Validation layers immediately report: "Command buffer is already in use by a previous submission." The fence prevents the CPU from overwriting a command buffer the GPU is still executing. - Delete vert.spv and run: The
readSpvFile()function catches the missing file, prints a clear error message explaining exactly which glslc command to run, then exits cleanly with proper cleanup. This shows correct error handling for file-based shader loading.
Koenig Original AI-Courseware · Day 4 Complete