Demo 15 · Module 10Vulkan Environment — Instance, Layers, Physical Device
VkInstance, Validation Layers, Physical Device Enumeration
Creates a VkInstance with validation layers, lists all available validation layers with descriptions, requests GLFW surface extensions, then enumerates every GPU in the system — printing name, type, API version, driver version, vendor ID, max texture size, queue families (with GRAPHICS/COMPUTE/TRANSFER flags), and memory heaps with sizes. Creates a VkSurfaceKHR via GLFW to prove surface extension support, opens a window to confirm, then shuts down. No logical device yet — this is a pure environment probe and query demo.
💻 Project: D15_VulkanEnv ⏱ ~35 min 266 lines New: VkInstance · validation layers · VkPhysicalDevice · VkSurfaceKHR
BUILDS ON: Nothing = Vulkan environment proof — window opens, GPU listed ✓
🎯 What You Will See
Terminal: All installed validation layers listed with descriptions
Terminal: All GPUs enumerated — name, type, driver version, queue families, VRAM heap sizes
Terminal: VkSurfaceKHR created successfully via GLFW (proves surface extension works)
Window: GLFW window opens titled "Demo 15 — Vulkan Environment" — close it to exit
No logical device — D16 adds that. This demo is query-only.
💡 D15 = The Vulkan Census — Inventory Before Building

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.

Vulkan Object Hierarchy — All Four Demos Build This Tree
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 window
CMakeLists.txt
CMakeLists.txt
CMake
cmake_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
)
src/main.cpp
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  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;
}
Build & Run
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
Expected Terminal Output
=== RR Graphics Lab — Demo 15: Vulkan Env ===

[GLFW] Vulkan supported: YES

[Validation Layers] 6 available:
VK_LAYER_KHRONOS_validation — Khronos validation layer
VK_LAYER_LUNARG_monitor — FPS monitoring layer
... (all installed layers listed with descriptions)

[Validation] VK_LAYER_KHRONOS_validation present: YES
[Extensions] Requesting 3 instance extensions:
VK_KHR_surface
VK_KHR_win32_surface
VK_EXT_debug_utils
[Instance] Validation layers ENABLED
[Instance] VkInstance created successfully: 0x...

[Physical Devices] 2 Vulkan-capable GPU(s) found:
GPU 0: NVIDIA GeForce RTX 3070
Type: Discrete GPU
API version: 1.3.260
Driver ver: 531.41.0
Queue families: 3
[0] count=16 flags=GRAPHICS COMPUTE TRANSFER
[1] count=2 flags=COMPUTE TRANSFER
[2] count=8 flags=TRANSFER
Memory heaps: 2
[0] 8192 MB (device-local / VRAM)
[1] 16383 MB (system RAM)

[Surface] VkSurfaceKHR created via GLFW — VK_KHR_surface working
Window open. Close it to exit.

[Cleanup] All Vulkan objects destroyed. Done.
🔬 Break-to-Learn Experiments
  • Intentionally break the sType field: In VkApplicationInfo, change sType = VK_STRUCTURE_TYPE_APPLICATION_INFO to sType = 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 print features.geometryShader, features.tessellationShader, features.samplerAnisotropy. These must be explicitly enabled in D16's VkDeviceCreateInfo before use.
Demo 16 · Module 11Logical Device, Queues, Command Pool, Command Buffer
VkDevice, VkQueue, VkCommandPool, VkCommandBuffer — Connecting to the GPU
Builds on Demo 15's instance and surface. Selects the best physical device (scoring discrete GPU higher), finds both graphics and present queue family indices, creates the logical VkDevice with the VK_KHR_swapchain extension enabled, retrieves graphics and present queues. Then creates a VkCommandPool and allocates a VkCommandBuffer — recording an empty begin/end to prove the API works. Opens a window that stays open until closed. No swapchain, no render loop — that comes in Demo 17.
💻 Project: D16_VulkanDevice ⏱ ~35 min 275 lines New: VkDevice · VkQueue · VkCommandPool · VkCommandBuffer
BUILDS ON: Demo 15 (instance + surface) + logical device + queues + command pool + buffer = Device ready, command buffer recorded, window open ✓
🎯 What You Will See
Terminal: VkInstance and surface created, physical device selected with name
Terminal: Graphics and present queue family indices printed (usually same on desktop)
Terminal: VkDevice created, both queues retrieved, command pool and buffer allocated
Terminal: "Command buffer recorded (empty begin/end) — no crash = API working"
Window: Opens and stays open — close it to trigger cleanup and exit
💡 VkDevice = Your Logical Handle to the GPU

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.

CMakeLists.txt
CMakeLists.txt
CMake
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
)
src/main.cpp
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  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;
}
Build & Run
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
Expected Terminal Output
=== RR Graphics Lab — Demo 16: Vulkan Device ===

[1] VkInstance created
[2] VkSurfaceKHR created
[3] Physical device selected: NVIDIA GeForce RTX 3070
[4] Queue families:
Graphics family index: 0
Present family index: 0
Same family: YES (typical on desktop GPUs)
[5] VkDevice (logical device) created: 0x...

[6] Queues retrieved:
Graphics queue: 0x...
Present queue: 0x...
[7] VkCommandPool created: 0x...
[8] VkCommandBuffer allocated: 0x...

[9] Command buffer recorded (empty begin/end) — no crash = API working

Summary:
VkInstance → VkPhysicalDevice (selected: NVIDIA GeForce RTX 3070)
VkPhysicalDevice → VkDevice (logical interface)
VkDevice → VkQueue (graphics + present)
VkDevice → VkCommandPool → VkCommandBuffer

Window open. Close to exit.
[Cleanup] Done.
🔬 Break-to-Learn Experiments
  • Intentionally use the wrong queue family index: Change qfi.graphicsFamily.value() to a hard-coded 99 in 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_TRUE in 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 = 1 to 3 and declare VkCommandBuffer cmdBufs[3]. Run. Observe: a pool allocates efficiently. In Demo 17, we allocate one buffer per swapchain image (typically 3).
Demo 17 · Module 11Swapchain, Render Pass, Framebuffers, Clear Loop
VkSwapchainKHR, VkRenderPass, VkFramebuffer — First Pixels on Screen
Adds the full windowed rendering stack on top of Demo 16's device. Queries surface capabilities, selects the best colour format (B8G8R8A8_SRGB) and present mode (MAILBOX preferred, FIFO fallback), creates VkSwapchainKHR, wraps each image in a VkImageView, defines a VkRenderPass with clear/store load ops, and creates one VkFramebuffer per swapchain image. Runs a full acquire–clear–present frame loop with semaphore and fence synchronisation. Window clears to navy blue every frame. No pipeline, no shaders, no geometry — that is Demo 18.
💻 Project: D17_VulkanSwapchain ⏱ ~35 min 442 lines New: VkSwapchainKHR · VkImageView · VkRenderPass · VkFramebuffer · present modes · frame sync
BUILDS ON: Demo 16 (device + command buffer) + swapchain + render pass + acquire → clear → present loop = Navy window clearing at vsync, semaphores + fence active ✓
🎯 What You Will See
Terminal: Swapchain support details — image count range, format count, present mode count
Terminal: Chosen format (B8G8R8A8_SRGB) and present mode (MAILBOX or FIFO) with reasoning
Terminal: Swapchain created with N images, image views created, render pass created, framebuffers created
Window: Navy blue (0.04, 0.08, 0.18) clearing every frame — first real Vulkan pixels
ESC to quit. No geometry yet — pipeline is Demo 18.
💡 glfwSwapBuffers() Unpacked — Three Explicit Steps

In 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.

CMakeLists.txt
CMakeLists.txt
CMake
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
)
src/main.cpp
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  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;
}
Build & Run
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
Expected Terminal Output
=== RR Graphics Lab — Demo 17: Swapchain ===

[1] GPU: NVIDIA GeForce RTX 3070
[2] VkDevice + queues ready
[3] Swapchain support:
Image count range: 2 – 8
Surface formats available: 4
Present modes available: 3
Format: VK_FORMAT_B8G8R8A8_SRGB (preferred)
Present mode: MAILBOX (triple-buffer, no tearing)
Extent: 900×600
[4] VkSwapchainKHR created: 3 images (900×600)
[5] 3 VkImageViews created
[6] VkRenderPass created
[7] 3 VkFramebuffers created

[8] Sync objects: semaphores + fence ready
Rendering navy clear colour each frame.
ESC to quit.

[Navy blue window — clearing at vsync. No geometry yet.]
[Cleanup] Done.
🔬 Break-to-Learn Experiments
  • 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 of glClearColor() — but note that the colour is set per-frame in VkRenderPassBeginInfo, not as global state.
  • Force FIFO present mode: Change choosePresentMode() to always return VK_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.
Demo 18 · Module 12Graphics Pipeline + First Triangle
VkPipeline, SPIR-V, vkCmdDraw — First Triangle on Screen
The Day 4 capstone. Adds a full 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.
💻 Project: D18_VulkanTriangle ⏱ ~45 min 621 lines New: VkShaderModule · VkPipeline · VkPipelineLayout · vkCmdDraw · glslc
BUILDS ON: Demo 17 (swapchain + render pass) + glslc-compiled SPIR-V + full VkPipeline + vkCmdDraw(3,1,0,0) = White triangle, navy background ✓
🎯 What You Will See
Window: Navy background, white triangle — positions hardcoded in shader.vert via gl_VertexIndex
Terminal: vert.spv and frag.spv file sizes printed after loading from disk
Terminal: "VkPipeline created" then "Shader modules destroyed" — bytecode released after compilation
No VkBuffer — vertices hardcoded in shader. Lab 4 adds a real vertex buffer.
ESC to quit. Validation layers: 0 errors.
⚠ Compile shaders before running

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.

💡 VkPipeline is Immutable — All State Baked at Creation

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.

Step 1 — Write and compile the shaders
Command
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
shader.vert — Vertex Shader (GLSL source)
shader.vert
GLSL
#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);
}
shader.frag — Fragment Shader (GLSL source)
shader.frag
GLSL
#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);
}
Step 2 — Build and run the C++ programme
CMakeLists.txt
CMakeLists.txt
CMake
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
)
src/main.cpp
src/main.cpp
C++
// ═══════════════════════════════════════════════════════════════════════════
//  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;
}
Command
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
Expected Terminal Output
=== RR Graphics Lab -- Demo 18: First Triangle ===

Requires: vert.spv and frag.spv in same folder as .exe
Compile: glslc shader.vert -o vert.spv
glslc shader.frag -o frag.spv

[1] GPU: Intel(R) Iris(R) Xe Graphics
[2] Swapchain + 3 images + views + framebuffers + renderpass ready
[3] Building VkPipeline:
Loading vert.spv and frag.spv...
vert.spv: 1484 bytes | frag.spv: 572 bytes
Shader modules created
VkPipeline created (shaders + rasterizer + blend + viewport)
Shader modules destroyed (bytecode released)

[4] Ready. Rendering white triangle. ESC to quit.

[White triangle on navy background — running at vsync]
[Validation layers: 0 errors]

[Cleanup] All Vulkan objects destroyed. Day 4 complete.
🔬 Break-to-Learn Experiments
  • Change the triangle colour: In shader.frag, change outColor = vec4(1.0, 1.0, 1.0, 1.0) to vec4(0.98, 0.57, 0.24, 1.0) (tactical orange). Recompile with glslc 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 three vec2 position 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.
← Day 3 Demos
By Raushan Ranjan (MCT | Educator)
Koenig Original AI-Courseware · Day 4 Complete
Day 4 Labs →