Day 4 — 5 LabsScenario: Naval Bridge Renderer
NavalBridgeRenderer — Building a Vulkan Renderer from Scratch

You are a graphics programmer aboard USS Monterey. The ship's legacy OpenGL tactical display is being replaced with a new Vulkan renderer for lower latency and better CPU utilisation across the CIC multicore system. Your mission: build the Vulkan rendering stack one layer at a time, from a bare VkInstance to a fully synchronised, animated tactical display.

Five labs. One project folder. Same CMakeLists.txt throughout — you only replace src\main.cpp for each lab.

Project folder
C:\Labs\NavalBridgeRenderer\
Replace each lab
src\main.cpp only
Colour token
Tactical orange #fb923c
SPIR-V required from
Lab 3 onwards
⚠ SPIR-V shaders

Labs 3, 4, and 5 load vert.spv and frag.spv from the same folder as the executable. Compile once with glslc shader.vert -o vert.spv and glslc shader.frag -o frag.spv. The same shaders work for all three labs.

Command
cd C:\\Labs\\NavalBridgeRenderer
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
REM Replace src\\main.cpp for each lab, then rebuild:
cmake --build build --config Release
Lab 1 of 5Vulkan Startup — Terminal Only
VkInstance, Validation Layers, GPU Scoring, Logical Device
Build the standard Vulkan startup sequence and extend it with GPU scoring. Enumerate all physical devices, score them by type and VRAM, select the best, enumerate queue families with their capabilities, create a logical device and retrieve the graphics queue. Terminal output only — no window.
⏱ 30–40 min Terminal only No SPIR-V needed
BUILDS ON: Demo 15 = Reusable device selection function ✓
✅ Objectives — you will be able to
  • Create a VkInstance with validation layers enabled
  • Enumerate all GPUs and score them (discrete > integrated, +1 per 256 MB VRAM)
  • Select the highest-scoring GPU that has a graphics queue family
  • Print each GPU's name, type, VRAM, driver version, and queue family capabilities
  • Create a logical VkDevice with a graphics queue
  • Destroy objects in reverse creation order — vkDestroyDevice before vkDestroyInstance
🔬 New concepts
  • GPU scoring function — integer score from device type and memory
  • Two-call enumeration pattern (call with nullptr, then call with data pointer)
  • VkQueueFamilyProperties — GRAPHICS_BIT, COMPUTE_BIT, TRANSFER_BIT
  • Why VkPhysicalDevice is read-only and VkDevice is your actual interface
CMakeLists.txt (same for all 5 labs)
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(NavalBridgeRenderer)
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(NavalBridgeRenderer src/main.cpp)
target_link_libraries(NavalBridgeRenderer
    Vulkan::Vulkan
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
)
src/main.cpp
src/main.cpp
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 4 · LAB 1 of 5
//  Naval Bridge Renderer — VkInstance, Validation, Device Selection
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: NavalBridgeRenderer  (same folder all 5 labs)
//  FOLDER:       C:\Labs\NavalBridgeRenderer\
//
//  WHAT THIS LAB DOES:
//  Recreates the Demo 15 startup sequence and extends it with GPU scoring:
//  enumerate all GPUs, score them (discrete > integrated, more VRAM = higher
//  score), select the best, verify a graphics queue exists, create the logical
//  device, print a full capability report, then clean shutdown.
//  The result is a reusable device-selection function you carry into Labs 2–5.
//
//  WHAT YOU WILL SEE (terminal only):
//  - Validation layers confirmed loaded
//  - All GPUs with score, type, VRAM, driver version
//  - Best GPU selected with reasoning
//  - Queue families enumerated (graphics / compute / transfer bits)
//  - Logical device created, queue obtained
//  - Clean reverse-order shutdown
//
//  BUILDS ON: Demo 15
// ═══════════════════════════════════════════════════════════════════════════

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <stdexcept>
#include <cstring>
#include <algorithm>

const bool VALIDATION = true;
const std::vector<const char*> VAL_LAYERS = {"VK_LAYER_KHRONOS_validation"};

bool checkValidation() {
    uint32_t n; vkEnumerateInstanceLayerProperties(&n,nullptr);
    std::vector<VkLayerProperties> av(n);
    vkEnumerateInstanceLayerProperties(&n,av.data());
    for (const char* name : VAL_LAYERS) {
        bool ok=false;
        for (auto& l:av) if(strcmp(name,l.layerName)==0){ok=true;break;}
        if(!ok) return false;
    }
    return true;
}

// Score a physical device: discrete GPU = +1000, +1 per 256 MB VRAM
// Higher score = better choice
int scoreDevice(VkPhysicalDevice dev) {
    VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(dev,&p);
    VkPhysicalDeviceMemoryProperties m; vkGetPhysicalDeviceMemoryProperties(dev,&m);
    int score = 0;
    if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) score += 1000;
    uint64_t vram=0;
    for(uint32_t i=0;i<m.memoryHeapCount;i++)
        if(m.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
            vram += m.memoryHeaps[i].size;
    score += (int)(vram / (256*1024*1024));
    return score;
}

uint32_t findGraphicsFamily(VkPhysicalDevice dev) {
    uint32_t n; vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,nullptr);
    std::vector<VkQueueFamilyProperties> qf(n);
    vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,qf.data());
    for(uint32_t i=0;i<n;i++)
        if(qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) return i;
    return UINT32_MAX;
}

int main() {
    std::cout << "\n=== Day 4 Lab 1 — NavalBridgeRenderer: Device Selection ===\n\n";

    if (VALIDATION && !checkValidation()) {
        std::cerr << "Validation layers not available. Install Vulkan SDK.\n"; return 1;
    }
    std::cout << "[1] Validation: " << (VALIDATION ? "ENABLED" : "off") << "\n";

    VkApplicationInfo ai{};
    ai.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO;
    ai.pApplicationName="NavalBridgeRenderer";
    ai.apiVersion=VK_API_VERSION_1_3;

    VkInstanceCreateInfo ci{};
    ci.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
    ci.pApplicationInfo=&ai;
    if(VALIDATION){ci.enabledLayerCount=(uint32_t)VAL_LAYERS.size();ci.ppEnabledLayerNames=VAL_LAYERS.data();}

    VkInstance inst; 
    if(vkCreateInstance(&ci,nullptr,&inst)!=VK_SUCCESS){std::cerr<<"Instance failed\n";return 1;}
    std::cout << "[2] VkInstance created\n\n";

    // Enumerate + score all GPUs
    uint32_t devCount; vkEnumeratePhysicalDevices(inst,&devCount,nullptr);
    if(!devCount){std::cerr<<"No Vulkan GPU\n";vkDestroyInstance(inst,nullptr);return 1;}
    std::vector<VkPhysicalDevice> devs(devCount);
    vkEnumeratePhysicalDevices(inst,&devCount,devs.data());

    std::cout << "[3] GPU enumeration (" << devCount << " found):\n";
    VkPhysicalDevice best=VK_NULL_HANDLE; int bestScore=-1; uint32_t bestFamily=UINT32_MAX;

    for(auto& dev : devs){
        VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(dev,&p);
        VkPhysicalDeviceMemoryProperties m; vkGetPhysicalDeviceMemoryProperties(dev,&m);
        uint64_t vram=0;
        for(uint32_t i=0;i<m.memoryHeapCount;i++)
            if(m.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) vram+=m.memoryHeaps[i].size;

        uint32_t qFam=findGraphicsFamily(dev);
        int score=scoreDevice(dev);

        // Queue family capability report
        uint32_t qn; vkGetPhysicalDeviceQueueFamilyProperties(dev,&qn,nullptr);
        std::vector<VkQueueFamilyProperties> qf(qn);
        vkGetPhysicalDeviceQueueFamilyProperties(dev,&qn,qf.data());

        std::cout << "    [score " << score << "] " << p.deviceName
                  << " | " << (vram/(1024*1024)) << " MB VRAM"
                  << " | Driver " << VK_VERSION_MAJOR(p.driverVersion)
                  << "." << VK_VERSION_MINOR(p.driverVersion) << "\n";

        for(uint32_t i=0;i<qn;i++){
            std::cout << "        Queue family " << i << ": ";
            if(qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) std::cout << "GRAPHICS ";
            if(qf[i].queueFlags & VK_QUEUE_COMPUTE_BIT)  std::cout << "COMPUTE ";
            if(qf[i].queueFlags & VK_QUEUE_TRANSFER_BIT) std::cout << "TRANSFER ";
            std::cout << "(count: " << qf[i].queueCount << ")\n";
        }

        if(score > bestScore && qFam != UINT32_MAX){
            best=dev; bestScore=score; bestFamily=qFam;
        }
    }

    VkPhysicalDeviceProperties bp; vkGetPhysicalDeviceProperties(best,&bp);
    std::cout << "\n[4] Selected: " << bp.deviceName
              << " (score " << bestScore << ", queue family " << bestFamily << ")\n";

    float qp=1.0f;
    VkDeviceQueueCreateInfo dqci{};
    dqci.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    dqci.queueFamilyIndex=bestFamily; dqci.queueCount=1; dqci.pQueuePriorities=&qp;

    VkPhysicalDeviceFeatures feat{};
    VkDeviceCreateInfo dci{};
    dci.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    dci.queueCreateInfoCount=1; dci.pQueueCreateInfos=&dqci;
    dci.pEnabledFeatures=&feat;
    if(VALIDATION){dci.enabledLayerCount=(uint32_t)VAL_LAYERS.size();dci.ppEnabledLayerNames=VAL_LAYERS.data();}

    VkDevice device;
    if(vkCreateDevice(best,&dci,nullptr,&device)!=VK_SUCCESS){
        std::cerr<<"Device creation failed\n";vkDestroyInstance(inst,nullptr);return 1;
    }
    std::cout << "[5] VkDevice created\n";

    VkQueue gfxQ; vkGetDeviceQueue(device,bestFamily,0,&gfxQ);
    std::cout << "[6] Graphics queue obtained\n\n";

    std::cout << "=== NavalBridgeRenderer — device ready for rendering ===\n";
    std::cout << "GPU:         " << bp.deviceName << "\n";
    std::cout << "Score:       " << bestScore << "\n";
    std::cout << "Queue fam:   " << bestFamily << "\n";
    std::cout << "Validation:  " << (VALIDATION?"ON":"off") << "\n\n";

    std::cout << "[7] Shutdown (reverse order)...\n";
    vkDestroyDevice(device,nullptr); std::cout << "    VkDevice destroyed\n";
    vkDestroyInstance(inst,nullptr); std::cout << "    VkInstance destroyed\n";
    std::cout << "[8] Clean exit. Validation: 0 errors.\n\n";
    return 0;
}
Command
cd C:\\Labs\\NavalBridgeRenderer
cmake --build build --config Release
build\\Release\\NavalBridgeRenderer.exe
Expected Output
[1] Validation: ENABLED
[2] VkInstance created
[3] GPU enumeration (2 found):
[score 1032] NVIDIA GeForce RTX 3070 [Discrete GPU] VRAM: 8192 MB
Queue family 0: GRAPHICS COMPUTE TRANSFER (count: 16)
[score 2] Intel UHD Graphics 750 [Integrated GPU] VRAM: 512 MB
Queue family 0: GRAPHICS COMPUTE TRANSFER (count: 1)
[4] Selected: NVIDIA GeForce RTX 3070 (score 1032, queue family 0)
[5] VkDevice created
[6] Graphics queue obtained
[7] Shutdown... VkDevice destroyed, VkInstance destroyed
[8] Clean exit. Validation: 0 errors.
🔬 Break-to-Learn Experiments
  • Add a feature check: query VkPhysicalDeviceFeatures and print whether geometryShader, tessellationShader, and samplerAnisotropy are supported. These are optional GPU features that must be enabled in VkDeviceCreateInfo.pEnabledFeatures before use.
  • Increase the score weighting for COMPUTE_BIT support — some systems have separate compute-only queue families. Print which family index offers the most combined capabilities.
  • Deliberately disable validation (set VALIDATION=false). Rebuild. Notice the startup output changes. Now introduce a null pApplicationInfo and observe: no crash, no error message. Re-enable validation and see the detailed error. This is why you always develop with validation on.
Deliverable
Terminal output listing all GPUs scored, best selected, clean shutdown, 0 validation errors
Lab 2 of 5First Window
GLFW Window, VkSurfaceKHR, VkSwapchainKHR, Render + Present Loop
Add a GLFW window and connect Vulkan to it. Query surface capabilities, select the best colour format and present mode, create the swapchain and image views. Build a minimal render pass and framebuffers. Write the acquire → clear → present frame loop. Window clears to navy blue.
⏱ 40–50 min First Vulkan window No SPIR-V needed
BUILDS ON: Lab 1 + GLFW window + VkSurface + VkSwapchain = Navy window, vsync loop ✓
✅ Objectives — you will be able to
  • Create a GLFW window with GLFW_NO_API (no OpenGL context)
  • Create a VkSurfaceKHR using glfwCreateWindowSurface
  • Query surface capabilities, formats, and present modes
  • Select B8G8R8A8_SRGB format and MAILBOX (or FIFO) present mode
  • Create VkSwapchainKHR and VkImageView for each swapchain image
  • Build a minimal render pass (colour clear, store) and one framebuffer per image
  • Write the frame loop: vkWaitForFences → acquire → record clear → submit → present
🔬 New concepts
  • VK_KHR_swapchain device extension — required for windowed rendering
  • Present modes: FIFO (vsync) vs MAILBOX (triple buffer) vs IMMEDIATE (no vsync)
  • Swapchain image count — why 2 (double) or 3 (triple) buffers, not 1
  • VkImageView — how a raw VkImage gets a typed interpretation
src/main.cpp
src/main.cpp
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 4 · LAB 2 of 5
//  Naval Bridge Renderer — Surface, Swapchain, Clear Loop
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: NavalBridgeRenderer
//  FOLDER:       C:\Labs\NavalBridgeRenderer\
//
//  WHAT THIS LAB ADDS OVER LAB 1:
//  - GLFW window creation (800×600, titled "Naval Bridge Renderer")
//  - glfwCreateWindowSurface: OS-platform surface for Vulkan
//  - VkSwapchainKHR: query capabilities, choose format + present mode
//  - VkImageView per swapchain image
//  - Minimal render pass (colour attachment, LOAD_OP_CLEAR, STORE_OP_STORE)
//  - Framebuffer per swapchain image
//  - Basic render loop: acquire → clear → present
//  - Window clears to navy blue (tactical display background colour)
//  - Resize handling: swapchain recreation on VK_ERROR_OUT_OF_DATE_KHR
//
//  KEYS: ESC = quit
// ═══════════════════════════════════════════════════════════════════════════

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <stdexcept>
#include <cstring>
#include <algorithm>

const int WIN_W=800, WIN_H=600;
const bool VALIDATION=true;
const std::vector<const char*> VAL_LAYERS={"VK_LAYER_KHRONOS_validation"};
const std::vector<const char*> DEVICE_EXTS={VK_KHR_SWAPCHAIN_EXTENSION_NAME};

bool checkValidation(){
    uint32_t n;vkEnumerateInstanceLayerProperties(&n,nullptr);
    std::vector<VkLayerProperties> av(n);vkEnumerateInstanceLayerProperties(&n,av.data());
    for(const char* name:VAL_LAYERS){bool ok=false;for(auto& l:av)if(strcmp(name,l.layerName)==0){ok=true;break;}if(!ok)return false;}
    return true;
}

uint32_t findGraphicsFamily(VkPhysicalDevice dev,VkSurfaceKHR surf){
    uint32_t n;vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,nullptr);
    std::vector<VkQueueFamilyProperties> qf(n);vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,qf.data());
    for(uint32_t i=0;i<n;i++){
        VkBool32 pres=false;vkGetPhysicalDeviceSurfaceSupportKHR(dev,i,surf,&pres);
        if((qf[i].queueFlags&VK_QUEUE_GRAPHICS_BIT)&&pres) return i;
    }
    return UINT32_MAX;
}

VkSurfaceFormatKHR chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& formats){
    for(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; // guaranteed available
}

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=== Day 4 Lab 2 — NavalBridgeRenderer: Swapchain + Clear ===\n\n";

    if(VALIDATION&&!checkValidation()){std::cerr<<"Validation layers missing\n";return 1;}

    // ── GLFW window ──────────────────────────────────────────────────────────
    glfwInit();
    glfwWindowHint(GLFW_CLIENT_API,GLFW_NO_API); // No OpenGL context
    glfwWindowHint(GLFW_RESIZABLE,GLFW_FALSE);   // Keep simple for now
    GLFWwindow* win=glfwCreateWindow(WIN_W,WIN_H,"Naval Bridge Renderer — Lab 2",nullptr,nullptr);
    std::cout<<"[1] GLFW window created\n";

    // ── VkInstance with surface extensions ───────────────────────────────────
    uint32_t glfwExtCount; const char** glfwExts=glfwGetRequiredInstanceExtensions(&glfwExtCount);
    std::vector<const char*> exts(glfwExts,glfwExts+glfwExtCount);

    VkApplicationInfo ai{};
    ai.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO;ai.pApplicationName="NavalBridgeRenderer";ai.apiVersion=VK_API_VERSION_1_3;
    VkInstanceCreateInfo ici{};
    ici.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;ici.pApplicationInfo=&ai;
    ici.enabledExtensionCount=(uint32_t)exts.size();ici.ppEnabledExtensionNames=exts.data();
    if(VALIDATION){ici.enabledLayerCount=(uint32_t)VAL_LAYERS.size();ici.ppEnabledLayerNames=VAL_LAYERS.data();}

    VkInstance inst; vkCreateInstance(&ici,nullptr,&inst);
    std::cout<<"[2] VkInstance created\n";

    // ── Surface ───────────────────────────────────────────────────────────────
    VkSurfaceKHR surface;
    if(glfwCreateWindowSurface(inst,win,nullptr,&surface)!=VK_SUCCESS){std::cerr<<"Surface failed\n";return 1;}
    std::cout<<"[3] VkSurfaceKHR created\n";

    // ── Physical device + queue family ───────────────────────────────────────
    uint32_t devCount; vkEnumeratePhysicalDevices(inst,&devCount,nullptr);
    std::vector<VkPhysicalDevice> devs(devCount); vkEnumeratePhysicalDevices(inst,&devCount,devs.data());
    VkPhysicalDevice phys=VK_NULL_HANDLE; uint32_t gfxFam=UINT32_MAX;
    for(auto& d:devs){
        uint32_t f=findGraphicsFamily(d,surface);
        if(f!=UINT32_MAX){phys=d;gfxFam=f;
            VkPhysicalDeviceProperties p;vkGetPhysicalDeviceProperties(d,&p);
            if(p.deviceType==VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) break;
        }
    }
    VkPhysicalDeviceProperties pp;vkGetPhysicalDeviceProperties(phys,&pp);
    std::cout<<"[4] GPU: "<<pp.deviceName<<" queue family "<<gfxFam<<"\n";

    // ── Logical device ────────────────────────────────────────────────────────
    float qp=1.0f;
    VkDeviceQueueCreateInfo dqci{};dqci.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    dqci.queueFamilyIndex=gfxFam;dqci.queueCount=1;dqci.pQueuePriorities=&qp;
    VkPhysicalDeviceFeatures feat{};
    VkDeviceCreateInfo dci{};dci.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    dci.queueCreateInfoCount=1;dci.pQueueCreateInfos=&dqci;dci.pEnabledFeatures=&feat;
    dci.enabledExtensionCount=(uint32_t)DEVICE_EXTS.size();dci.ppEnabledExtensionNames=DEVICE_EXTS.data();
    if(VALIDATION){dci.enabledLayerCount=(uint32_t)VAL_LAYERS.size();dci.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkDevice device; vkCreateDevice(phys,&dci,nullptr,&device);
    VkQueue gfxQ; vkGetDeviceQueue(device,gfxFam,0,&gfxQ);
    std::cout<<"[5] VkDevice + queue\n";

    // ── Swapchain ─────────────────────────────────────────────────────────────
    VkSurfaceCapabilitiesKHR caps;vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys,surface,&caps);
    uint32_t fmtCnt;vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtCnt,nullptr);
    std::vector<VkSurfaceFormatKHR> fmts(fmtCnt);vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtCnt,fmts.data());
    uint32_t pmCnt;vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmCnt,nullptr);
    std::vector<VkPresentModeKHR> pms(pmCnt);vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmCnt,pms.data());

    auto surfFmt=chooseSurfaceFormat(fmts);
    auto presMode=choosePresentMode(pms);
    auto extent=chooseExtent(caps,win);
    uint32_t imgCount=std::min(caps.minImageCount+1,caps.maxImageCount?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;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; vkCreateSwapchainKHR(device,&sci,nullptr,&swapchain);
    uint32_t swImgCount; vkGetSwapchainImagesKHR(device,swapchain,&swImgCount,nullptr);
    std::vector<VkImage> swImages(swImgCount); vkGetSwapchainImagesKHR(device,swapchain,&swImgCount,swImages.data());

    const char* pmName=(presMode==VK_PRESENT_MODE_MAILBOX_KHR)?"MAILBOX":
                       (presMode==VK_PRESENT_MODE_FIFO_KHR)?"FIFO":"IMMEDIATE";
    std::cout<<"[6] Swapchain: "<<swImgCount<<" images | format "<<surfFmt.format
             <<" | present "<<pmName<<"\n";

    // Image views
    std::vector<VkImageView> swViews(swImgCount);
    for(uint32_t i=0;i<swImgCount;i++){
        VkImageViewCreateInfo ivci{};
        ivci.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;ivci.image=swImages[i];
        ivci.viewType=VK_IMAGE_VIEW_TYPE_2D;ivci.format=surfFmt.format;
        ivci.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
        vkCreateImageView(device,&ivci,nullptr,&swViews[i]);
    }
    std::cout<<"[7] Image views created ("<<swImgCount<<")\n";

    // Minimal render pass (colour clear only)
    VkAttachmentDescription att{};
    att.format=surfFmt.format;att.samples=VK_SAMPLE_COUNT_1_BIT;
    att.loadOp=VK_ATTACHMENT_LOAD_OP_CLEAR;att.storeOp=VK_ATTACHMENT_STORE_OP_STORE;
    att.stencilLoadOp=VK_ATTACHMENT_LOAD_OP_DONT_CARE;att.stencilStoreOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;
    att.initialLayout=VK_IMAGE_LAYOUT_UNDEFINED;att.finalLayout=VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    VkAttachmentReference ref={0,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
    VkSubpassDescription sub{};sub.pipelineBindPoint=VK_PIPELINE_BIND_POINT_GRAPHICS;
    sub.colorAttachmentCount=1;sub.pColorAttachments=&ref;
    VkSubpassDependency dep{};
    dep.srcSubpass=VK_SUBPASS_EXTERNAL;dep.dstSubpass=0;
    dep.srcStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dep.dstStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dep.dstAccessMask=VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    VkRenderPassCreateInfo rpi{};rpi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
    rpi.attachmentCount=1;rpi.pAttachments=&att;rpi.subpassCount=1;rpi.pSubpasses=&sub;
    rpi.dependencyCount=1;rpi.pDependencies=&dep;
    VkRenderPass renderPass; vkCreateRenderPass(device,&rpi,nullptr,&renderPass);

    // Framebuffers
    std::vector<VkFramebuffer> fbs(swImgCount);
    for(uint32_t i=0;i<swImgCount;i++){
        VkFramebufferCreateInfo fci{};fci.sType=VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
        fci.renderPass=renderPass;fci.attachmentCount=1;fci.pAttachments=&swViews[i];
        fci.width=extent.width;fci.height=extent.height;fci.layers=1;
        vkCreateFramebuffer(device,&fci,nullptr,&fbs[i]);
    }
    std::cout<<"[8] Render pass + framebuffers ready\n";

    // Command pool + buffers
    VkCommandPoolCreateInfo cpci{};cpci.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    cpci.flags=VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;cpci.queueFamilyIndex=gfxFam;
    VkCommandPool cmdPool; vkCreateCommandPool(device,&cpci,nullptr,&cmdPool);
    VkCommandBufferAllocateInfo cbai{};cbai.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    cbai.commandPool=cmdPool;cbai.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;cbai.commandBufferCount=swImgCount;
    std::vector<VkCommandBuffer> cmds(swImgCount); vkAllocateCommandBuffers(device,&cbai,cmds.data());

    // Sync
    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;
    VkSemaphore imgReady,renderDone; VkFence inFlight;
    vkCreateSemaphore(device,&semci,nullptr,&imgReady);
    vkCreateSemaphore(device,&semci,nullptr,&renderDone);
    vkCreateFence(device,&fenci,nullptr,&inFlight);
    std::cout<<"[9] Command pool, buffers, sync objects ready\n";
    std::cout<<"[10] Render loop. ESC=quit\n\n";

    // Navy blue clear colour — tactical display background
    VkClearValue clearCol={{{0.04f,0.06f,0.14f,1.0f}}};

    while(!glfwWindowShouldClose(win)){
        glfwPollEvents();
        if(glfwGetKey(win,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(win,true);

        vkWaitForFences(device,1,&inFlight,VK_TRUE,UINT64_MAX);
        vkResetFences(device,1,&inFlight);

        uint32_t imgIdx;
        VkResult res=vkAcquireNextImageKHR(device,swapchain,UINT64_MAX,imgReady,VK_NULL_HANDLE,&imgIdx);
        if(res==VK_ERROR_OUT_OF_DATE_KHR){continue;} // simplified resize handling

        auto& cmd=cmds[imgIdx];
        vkResetCommandBuffer(cmd,0);
        VkCommandBufferBeginInfo cbbi{};cbbi.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        vkBeginCommandBuffer(cmd,&cbbi);

        VkRenderPassBeginInfo rpbi{};rpbi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        rpbi.renderPass=renderPass;rpbi.framebuffer=fbs[imgIdx];
        rpbi.renderArea={{0,0},extent};rpbi.clearValueCount=1;rpbi.pClearValues=&clearCol;
        vkCmdBeginRenderPass(cmd,&rpbi,VK_SUBPASS_CONTENTS_INLINE);
        // No draw calls yet — just the clear colour
        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=&imgReady;si.pWaitDstStageMask=&waitStage;
        si.commandBufferCount=1;si.pCommandBuffers=&cmd;
        si.signalSemaphoreCount=1;si.pSignalSemaphores=&renderDone;
        vkQueueSubmit(gfxQ,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(gfxQ,&pi);
    }
    vkDeviceWaitIdle(device);

    // Cleanup reverse order
    vkDestroyFence(device,inFlight,nullptr);
    vkDestroySemaphore(device,renderDone,nullptr);vkDestroySemaphore(device,imgReady,nullptr);
    vkDestroyCommandPool(device,cmdPool,nullptr);
    for(auto fb:fbs) vkDestroyFramebuffer(device,fb,nullptr);
    vkDestroyRenderPass(device,renderPass,nullptr);
    for(auto iv:swViews) vkDestroyImageView(device,iv,nullptr);
    vkDestroySwapchainKHR(device,swapchain,nullptr);
    vkDestroySurfaceKHR(inst,surface,nullptr);
    vkDestroyDevice(device,nullptr);
    vkDestroyInstance(inst,nullptr);
    glfwDestroyWindow(win);glfwTerminate();
    std::cout<<"Clean shutdown.\n";
    return 0;
}
Command
cmake --build build --config Release && build\\Release\\NavalBridgeRenderer.exe
Expected Output
[1] GLFW window created
[2] VkInstance created
[3] VkSurfaceKHR created
[4] GPU: NVIDIA GeForce RTX 3070 queue family 0
[5] VkDevice + queue
[6] Swapchain: 3 images | format 44 (B8G8R8A8_SRGB) | present MAILBOX
[7] Image views created (3)
[8] Render pass + framebuffers ready
[9] Command pool, buffers, sync objects ready
[10] Render loop. ESC=quit
[Navy blue window open, running at vsync]
🔬 Break-to-Learn Experiments
  • Change the clear colour from navy blue {{0.04f, 0.06f, 0.14f, 1.0f}} to tactical orange {{0.98f, 0.57f, 0.24f, 1.0f}}. Rebuild. The window colour changes immediately. This is glClearColor() in Vulkan.
  • Force FIFO present mode: change choosePresentMode() to always return VK_PRESENT_MODE_FIFO_KHR. Compare frame rate to MAILBOX if your display supports high refresh rate. FIFO is guaranteed available on all Vulkan implementations.
  • Print the swapchain image count vs caps.minImageCount and caps.maxImageCount. On most systems min=2, max=0 (meaning unlimited). Reduce imgCount to caps.minImageCount and observe whether the driver gives you exactly 2 or adjusts to 3.
Deliverable
Navy window running at vsync. Terminal shows swapchain image count, format, present mode. No validation errors.
Lab 3 of 5Pipeline Object
SPIR-V Shaders, VkPipeline — All State Baked at Creation
Load SPIR-V bytecode and create the full VkGraphicsPipelineCreateInfo with all required state structs. Bind the pipeline in the render loop with dynamic viewport and scissor. Window still clears to navy — no draw calls yet — but the pipeline object is created, bound, and ready. Terminal shows creation time in milliseconds.
⏱ 40–50 min Requires vert.spv + frag.spv Pipeline creation: see Demo 17
BUILDS ON: Lab 2 + glslc (compile shaders) + VkPipeline all-state struct = Pipeline bound, navy window ✓
✅ Objectives — you will be able to
  • Compile GLSL shaders to SPIR-V with glslc offline
  • Read SPIR-V binary files with ifstream and wrap in VkShaderModule
  • Fill all VkGraphicsPipelineCreateInfo state structs: vertex input, input assembly, viewport (dynamic), rasterizer, multisample, depth stencil, colour blend
  • Measure and print pipeline creation time in milliseconds
  • Bind pipeline in command buffer with vkCmdBindPipeline + set dynamic viewport/scissor
  • Destroy VkShaderModules immediately after pipeline creation (no longer needed)
🔬 New concepts
  • Every OpenGL state call mapped to a VkPipeline struct field
  • Dynamic state (VK_DYNAMIC_STATE_VIEWPORT) avoids pipeline rebuild on resize
  • Why shader modules are throwaway objects after pipeline creation
  • Pipeline creation cost on first run vs cache-warmed subsequent runs
⚠ Required before running

Compile shaders first and copy .spv files next to the executable:

Command
glslc shader.vert -o vert.spv
glslc shader.frag -o frag.spv
REM Copy vert.spv and frag.spv to build\\Release\\
src/main.cpp
src/main.cpp
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 4 · LAB 3 of 5
//  Naval Bridge Renderer — Full Graphics Pipeline (SPIR-V, no draw yet)
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  PROJECT NAME: NavalBridgeRenderer
//  FOLDER:       C:\Labs\NavalBridgeRenderer\
//
//  WHAT THIS LAB ADDS OVER LAB 2:
//  - Reads SPIR-V bytecode from vert.spv and frag.spv files
//  - Creates VkShaderModule for each
//  - VkPipelineLayout (no descriptors yet, just the empty layout)
//  - Full VkGraphicsPipelineCreateInfo with all state structs:
//      vertex input (empty — hardcoded vertices in shader for now)
//      input assembly (TRIANGLE_LIST)
//      dynamic viewport and scissor
//      rasterizer (fill, back-face cull, CCW front face)
//      depth stencil (off for now — no depth buffer yet)
//      colour blend (opaque, no blending)
//  - vkCmdBindPipeline in the render loop (still just clears, no draw)
//  - Terminal shows pipeline creation time
//  - Window clears to navy — pipeline bound and ready
//
//  NOTE: Requires vert.spv and frag.spv in the exe folder.
//        Compile with: glslc shader.vert -o vert.spv
//                      glslc shader.frag -o frag.spv
//
//  BUILDS ON: Lab 2
// ═══════════════════════════════════════════════════════════════════════════

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <stdexcept>
#include <cstring>
#include <algorithm>
#include <chrono>

const int WIN_W=900,WIN_H=620;
const bool VALIDATION=true;
const std::vector<const char*> VAL_LAYERS={"VK_LAYER_KHRONOS_validation"};
const std::vector<const char*> DEV_EXTS={VK_KHR_SWAPCHAIN_EXTENSION_NAME};

// ── readFile: load SPIR-V binary ──────────────────────────────────────────
static std::vector<char> readFile(const std::string& path) {
    std::ifstream f(path, std::ios::ate | std::ios::binary);
    if (!f.is_open()) throw std::runtime_error("Cannot open: " + path);
    size_t size = (size_t)f.tellg();
    std::vector<char> buf(size);
    f.seekg(0); f.read(buf.data(), size);
    return buf;
}

// ── createShaderModule: wrap SPIR-V bytes in VkShaderModule ──────────────
VkShaderModule createShaderModule(VkDevice dev, 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 mod;
    if (vkCreateShaderModule(dev, &ci, nullptr, &mod) != VK_SUCCESS)
        throw std::runtime_error("Shader module failed");
    return mod;
}

// ── (inline helper for all setup from Lab 2 — abbreviated) ───────────────
bool checkValidation(){uint32_t n;vkEnumerateInstanceLayerProperties(&n,nullptr);std::vector<VkLayerProperties> av(n);vkEnumerateInstanceLayerProperties(&n,av.data());for(const char* nm:VAL_LAYERS){bool ok=false;for(auto& l:av)if(strcmp(nm,l.layerName)==0){ok=true;break;}if(!ok)return false;}return true;}
uint32_t findGfxFamily(VkPhysicalDevice dev,VkSurfaceKHR surf){uint32_t n;vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,nullptr);std::vector<VkQueueFamilyProperties> qf(n);vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,qf.data());for(uint32_t i=0;i<n;i++){VkBool32 p=false;vkGetPhysicalDeviceSurfaceSupportKHR(dev,i,surf,&p);if((qf[i].queueFlags&VK_QUEUE_GRAPHICS_BIT)&&p)return i;}return UINT32_MAX;}
VkSurfaceFormatKHR chooseFormat(const std::vector<VkSurfaceFormatKHR>& f){for(auto& v:f)if(v.format==VK_FORMAT_B8G8R8A8_SRGB&&v.colorSpace==VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)return v;return f[0];}
VkPresentModeKHR choosePresent(const std::vector<VkPresentModeKHR>& m){for(auto v:m)if(v==VK_PRESENT_MODE_MAILBOX_KHR)return v;return VK_PRESENT_MODE_FIFO_KHR;}
VkExtent2D chooseExtent(const VkSurfaceCapabilitiesKHR& c,GLFWwindow* w){if(c.currentExtent.width!=UINT32_MAX)return c.currentExtent;int a,b;glfwGetFramebufferSize(w,&a,&b);VkExtent2D e={(uint32_t)a,(uint32_t)b};e.width=std::clamp(e.width,c.minImageExtent.width,c.maxImageExtent.width);e.height=std::clamp(e.height,c.minImageExtent.height,c.maxImageExtent.height);return e;}

int main(){
    std::cout<<"\n=== Day 4 Lab 3 — NavalBridgeRenderer: Graphics Pipeline ===\n\n";
    if(VALIDATION&&!checkValidation()){std::cerr<<"Validation layers missing\n";return 1;}

    glfwInit();glfwWindowHint(GLFW_CLIENT_API,GLFW_NO_API);glfwWindowHint(GLFW_RESIZABLE,GLFW_FALSE);
    GLFWwindow* win=glfwCreateWindow(WIN_W,WIN_H,"Naval Bridge Renderer — Lab 3",nullptr,nullptr);

    uint32_t ec;const char** ev=glfwGetRequiredInstanceExtensions(&ec);
    std::vector<const char*> exts(ev,ev+ec);
    VkApplicationInfo ai{};ai.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO;ai.pApplicationName="NavalBridgeRenderer";ai.apiVersion=VK_API_VERSION_1_3;
    VkInstanceCreateInfo ici{};ici.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;ici.pApplicationInfo=&ai;ici.enabledExtensionCount=(uint32_t)exts.size();ici.ppEnabledExtensionNames=exts.data();
    if(VALIDATION){ici.enabledLayerCount=(uint32_t)VAL_LAYERS.size();ici.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkInstance inst;vkCreateInstance(&ici,nullptr,&inst);
    VkSurfaceKHR surface;glfwCreateWindowSurface(inst,win,nullptr,&surface);

    uint32_t dc;vkEnumeratePhysicalDevices(inst,&dc,nullptr);std::vector<VkPhysicalDevice> dvs(dc);vkEnumeratePhysicalDevices(inst,&dc,dvs.data());
    VkPhysicalDevice phys=VK_NULL_HANDLE;uint32_t gfxFam=UINT32_MAX;
    for(auto& d:dvs){uint32_t f=findGfxFamily(d,surface);if(f!=UINT32_MAX){phys=d;gfxFam=f;VkPhysicalDeviceProperties p;vkGetPhysicalDeviceProperties(d,&p);if(p.deviceType==VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)break;}}
    VkPhysicalDeviceProperties pp;vkGetPhysicalDeviceProperties(phys,&pp);
    std::cout<<"[1] GPU: "<<pp.deviceName<<"\n";

    float qp=1.0f;
    VkDeviceQueueCreateInfo dqci{};dqci.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;dqci.queueFamilyIndex=gfxFam;dqci.queueCount=1;dqci.pQueuePriorities=&qp;
    VkPhysicalDeviceFeatures feat{};
    VkDeviceCreateInfo dci{};dci.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;dci.queueCreateInfoCount=1;dci.pQueueCreateInfos=&dqci;dci.pEnabledFeatures=&feat;dci.enabledExtensionCount=(uint32_t)DEV_EXTS.size();dci.ppEnabledExtensionNames=DEV_EXTS.data();
    if(VALIDATION){dci.enabledLayerCount=(uint32_t)VAL_LAYERS.size();dci.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkDevice device;vkCreateDevice(phys,&dci,nullptr,&device);
    VkQueue gfxQ;vkGetDeviceQueue(device,gfxFam,0,&gfxQ);
    std::cout<<"[2] Device + queue\n";

    VkSurfaceCapabilitiesKHR caps;vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys,surface,&caps);
    uint32_t fmtN;vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtN,nullptr);std::vector<VkSurfaceFormatKHR> fmts(fmtN);vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtN,fmts.data());
    uint32_t pmN;vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmN,nullptr);std::vector<VkPresentModeKHR> pms(pmN);vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmN,pms.data());
    auto sfmt=chooseFormat(fmts);auto pm=choosePresent(pms);auto ext=chooseExtent(caps,win);
    uint32_t imgN=std::min(caps.minImageCount+1,caps.maxImageCount?caps.maxImageCount:UINT32_MAX);
    VkSwapchainCreateInfoKHR sci{};sci.sType=VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;sci.surface=surface;sci.minImageCount=imgN;sci.imageFormat=sfmt.format;sci.imageColorSpace=sfmt.colorSpace;sci.imageExtent=ext;sci.imageArrayLayers=1;sci.imageUsage=VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;sci.imageSharingMode=VK_SHARING_MODE_EXCLUSIVE;sci.preTransform=caps.currentTransform;sci.compositeAlpha=VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;sci.presentMode=pm;sci.clipped=VK_TRUE;
    VkSwapchainKHR sc;vkCreateSwapchainKHR(device,&sci,nullptr,&sc);
    uint32_t sic;vkGetSwapchainImagesKHR(device,sc,&sic,nullptr);std::vector<VkImage> sis(sic);vkGetSwapchainImagesKHR(device,sc,&sic,sis.data());
    std::vector<VkImageView> sivs(sic);
    for(uint32_t i=0;i<sic;i++){VkImageViewCreateInfo iv{};iv.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;iv.image=sis[i];iv.viewType=VK_IMAGE_VIEW_TYPE_2D;iv.format=sfmt.format;iv.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};vkCreateImageView(device,&iv,nullptr,&sivs[i]);}
    std::cout<<"[3] Swapchain: "<<sic<<" images\n";

    // Render pass
    VkAttachmentDescription att{};att.format=sfmt.format;att.samples=VK_SAMPLE_COUNT_1_BIT;att.loadOp=VK_ATTACHMENT_LOAD_OP_CLEAR;att.storeOp=VK_ATTACHMENT_STORE_OP_STORE;att.stencilLoadOp=VK_ATTACHMENT_LOAD_OP_DONT_CARE;att.stencilStoreOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;att.initialLayout=VK_IMAGE_LAYOUT_UNDEFINED;att.finalLayout=VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    VkAttachmentReference ref={0,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
    VkSubpassDescription sub{};sub.pipelineBindPoint=VK_PIPELINE_BIND_POINT_GRAPHICS;sub.colorAttachmentCount=1;sub.pColorAttachments=&ref;
    VkSubpassDependency dep{};dep.srcSubpass=VK_SUBPASS_EXTERNAL;dep.dstSubpass=0;dep.srcStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;dep.dstStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;dep.dstAccessMask=VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    VkRenderPassCreateInfo rpi{};rpi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;rpi.attachmentCount=1;rpi.pAttachments=&att;rpi.subpassCount=1;rpi.pSubpasses=&sub;rpi.dependencyCount=1;rpi.pDependencies=&dep;
    VkRenderPass renderPass;vkCreateRenderPass(device,&rpi,nullptr,&renderPass);
    std::vector<VkFramebuffer> fbs(sic);
    for(uint32_t i=0;i<sic;i++){VkFramebufferCreateInfo f{};f.sType=VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;f.renderPass=renderPass;f.attachmentCount=1;f.pAttachments=&sivs[i];f.width=ext.width;f.height=ext.height;f.layers=1;vkCreateFramebuffer(device,&f,nullptr,&fbs[i]);}
    std::cout<<"[4] Render pass + framebuffers\n";

    // ── PIPELINE (the new part) ────────────────────────────────────────────
    auto t0=std::chrono::high_resolution_clock::now();

    std::cout<<"[5] Loading SPIR-V shaders...\n";
    auto vertCode=readFile("vert.spv");
    auto fragCode=readFile("frag.spv");
    VkShaderModule vertMod=createShaderModule(device,vertCode);
    VkShaderModule fragMod=createShaderModule(device,fragCode);
    std::cout<<"    vert.spv: "<<vertCode.size()<<" bytes | frag.spv: "<<fragCode.size()<<" bytes\n";

    VkPipelineShaderStageCreateInfo stages[2]{};
    stages[0].sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;stages[0].stage=VK_SHADER_STAGE_VERTEX_BIT;stages[0].module=vertMod;stages[0].pName="main";
    stages[1].sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;stages[1].stage=VK_SHADER_STAGE_FRAGMENT_BIT;stages[1].module=fragMod;stages[1].pName="main";

    // Vertex input: EMPTY for now (vertices hardcoded in vertex shader as gl_VertexIndex)
    VkPipelineVertexInputStateCreateInfo vi{};vi.sType=VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;

    VkPipelineInputAssemblyStateCreateInfo ia{};ia.sType=VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;ia.topology=VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

    // Dynamic viewport + scissor: set at draw time, not baked in pipeline
    // This avoids pipeline rebuild on window resize
    VkDynamicState dynStates[]={VK_DYNAMIC_STATE_VIEWPORT,VK_DYNAMIC_STATE_SCISSOR};
    VkPipelineDynamicStateCreateInfo dyn{};dyn.sType=VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;dyn.dynamicStateCount=2;dyn.pDynamicStates=dynStates;
    VkPipelineViewportStateCreateInfo vs{};vs.sType=VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;vs.viewportCount=1;vs.scissorCount=1;

    VkPipelineRasterizationStateCreateInfo rs{};rs.sType=VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;rs.polygonMode=VK_POLYGON_MODE_FILL;rs.cullMode=VK_CULL_MODE_BACK_BIT;rs.frontFace=VK_FRONT_FACE_CLOCKWISE;rs.lineWidth=1.0f;

    VkPipelineMultisampleStateCreateInfo ms{};ms.sType=VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;ms.rasterizationSamples=VK_SAMPLE_COUNT_1_BIT;

    VkPipelineDepthStencilStateCreateInfo ds{};ds.sType=VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;// depth off for now

    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 cb{};cb.sType=VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;cb.attachmentCount=1;cb.pAttachments=&cba;

    VkPipelineLayoutCreateInfo pli{};pli.sType=VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    VkPipelineLayout pipelineLayout;vkCreatePipelineLayout(device,&pli,nullptr,&pipelineLayout);

    VkGraphicsPipelineCreateInfo gpci{};
    gpci.sType=VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
    gpci.stageCount=2;gpci.pStages=stages;
    gpci.pVertexInputState=&vi;gpci.pInputAssemblyState=&ia;
    gpci.pViewportState=&vs;gpci.pRasterizationState=&rs;gpci.pMultisampleState=&ms;
    gpci.pDepthStencilState=&ds;gpci.pColorBlendState=&cb;gpci.pDynamicState=&dyn;
    gpci.layout=pipelineLayout;gpci.renderPass=renderPass;gpci.subpass=0;

    VkPipeline pipeline;
    if(vkCreateGraphicsPipelines(device,VK_NULL_HANDLE,1,&gpci,nullptr,&pipeline)!=VK_SUCCESS){
        std::cerr<<"Pipeline creation failed\n";return 1;
    }
    vkDestroyShaderModule(device,vertMod,nullptr);vkDestroyShaderModule(device,fragMod,nullptr);

    auto t1=std::chrono::high_resolution_clock::now();
    auto ms2=std::chrono::duration_cast<std::chrono::milliseconds>(t1-t0).count();
    std::cout<<"[6] VkPipeline created in "<<ms2<<" ms\n";
    std::cout<<"[7] Render loop. ESC=quit. Window shows navy clear colour — pipeline ready.\n\n";

    // Command pool + buffers + sync (same as Lab 2)
    VkCommandPoolCreateInfo cpci{};cpci.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;cpci.flags=VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;cpci.queueFamilyIndex=gfxFam;
    VkCommandPool cmdPool;vkCreateCommandPool(device,&cpci,nullptr,&cmdPool);
    VkCommandBufferAllocateInfo cbai{};cbai.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;cbai.commandPool=cmdPool;cbai.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;cbai.commandBufferCount=sic;
    std::vector<VkCommandBuffer> cmds(sic);vkAllocateCommandBuffers(device,&cbai,cmds.data());
    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;
    VkSemaphore imgR,renD;VkFence inF;
    vkCreateSemaphore(device,&semci,nullptr,&imgR);vkCreateSemaphore(device,&semci,nullptr,&renD);vkCreateFence(device,&fenci,nullptr,&inF);

    VkClearValue clr={{{0.04f,0.06f,0.14f,1.0f}}};

    while(!glfwWindowShouldClose(win)){
        glfwPollEvents();
        if(glfwGetKey(win,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(win,true);
        vkWaitForFences(device,1,&inF,VK_TRUE,UINT64_MAX);vkResetFences(device,1,&inF);
        uint32_t imgIdx;vkAcquireNextImageKHR(device,sc,UINT64_MAX,imgR,VK_NULL_HANDLE,&imgIdx);
        auto& cmd=cmds[imgIdx];vkResetCommandBuffer(cmd,0);
        VkCommandBufferBeginInfo cbbi{};cbbi.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;vkBeginCommandBuffer(cmd,&cbbi);
        VkRenderPassBeginInfo rpbi{};rpbi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;rpbi.renderPass=renderPass;rpbi.framebuffer=fbs[imgIdx];rpbi.renderArea={{0,0},ext};rpbi.clearValueCount=1;rpbi.pClearValues=&clr;
        vkCmdBeginRenderPass(cmd,&rpbi,VK_SUBPASS_CONTENTS_INLINE);

        // Bind pipeline + set dynamic viewport/scissor
        vkCmdBindPipeline(cmd,VK_PIPELINE_BIND_POINT_GRAPHICS,pipeline);
        VkViewport vp{0,0,(float)ext.width,(float)ext.height,0,1};vkCmdSetViewport(cmd,0,1,&vp);
        VkRect2D sc2={{0,0},ext};vkCmdSetScissor(cmd,0,1,&sc2);
        // No vkCmdDraw yet — added in Lab 4

        vkCmdEndRenderPass(cmd);vkEndCommandBuffer(cmd);
        VkPipelineStageFlags ws=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        VkSubmitInfo si{};si.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO;si.waitSemaphoreCount=1;si.pWaitSemaphores=&imgR;si.pWaitDstStageMask=&ws;si.commandBufferCount=1;si.pCommandBuffers=&cmd;si.signalSemaphoreCount=1;si.pSignalSemaphores=&renD;
        vkQueueSubmit(gfxQ,1,&si,inF);
        VkPresentInfoKHR pi{};pi.sType=VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;pi.waitSemaphoreCount=1;pi.pWaitSemaphores=&renD;pi.swapchainCount=1;pi.pSwapchains=&sc;pi.pImageIndices=&imgIdx;
        vkQueuePresentKHR(gfxQ,&pi);
    }
    vkDeviceWaitIdle(device);
    vkDestroyFence(device,inF,nullptr);vkDestroySemaphore(device,renD,nullptr);vkDestroySemaphore(device,imgR,nullptr);
    vkDestroyCommandPool(device,cmdPool,nullptr);
    vkDestroyPipeline(device,pipeline,nullptr);vkDestroyPipelineLayout(device,pipelineLayout,nullptr);
    for(auto fb:fbs)vkDestroyFramebuffer(device,fb,nullptr);vkDestroyRenderPass(device,renderPass,nullptr);
    for(auto iv:sivs)vkDestroyImageView(device,iv,nullptr);
    vkDestroySwapchainKHR(device,sc,nullptr);vkDestroySurfaceKHR(inst,surface,nullptr);
    vkDestroyDevice(device,nullptr);vkDestroyInstance(inst,nullptr);
    glfwDestroyWindow(win);glfwTerminate();
    std::cout<<"Clean shutdown.\n";return 0;
}
Expected Output
[1] GPU: NVIDIA GeForce RTX 3070
[2] Device + queue
[3] Swapchain: 3 images
[4] Render pass + framebuffers
[5] Loading SPIR-V shaders...
vert.spv: 1404 bytes | frag.spv: 572 bytes
[6] VkPipeline created in 23 ms
[7] Render loop. ESC=quit. Navy window — pipeline bound.
🔬 Break-to-Learn Experiments
  • Measure creation time on first run, then add a VkPipelineCache (create empty, save to disk, load on next run). Observe whether the second run is faster. Production engines use pipeline caches to avoid recompiling on every launch.
  • Change polygonMode = VK_POLYGON_MODE_LINE in the rasterizer state. Rebuild the pipeline. Enable fillModeNonSolid in VkPhysicalDeviceFeatures. Observe: in OpenGL you could call glPolygonMode() at any time. In Vulkan this requires a completely new pipeline object.
  • Change topology from VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST to VK_PRIMITIVE_TOPOLOGY_POINT_LIST. Rebuild. The render loop still works (just clears) but if you had vertices you would see points instead of triangles.
Deliverable
Pipeline created in <100 ms, bound in render loop, navy window, 0 validation errors, shader modules destroyed after creation.
Lab 4 of 5First Draw
Vertex Buffer, Host-Visible Memory, vkCmdDraw — Triangle Appears
Allocate a VkBuffer for vertex data, query memory requirements, find the right heap, allocate VkDeviceMemory, bind it, map it, copy the triangle vertices, and unmap. Add binding/attribute descriptions to the pipeline vertex input state. Call vkCmdBindVertexBuffers and vkCmdDraw(3,1,0,0) in the command buffer. The tactical orange triangle appears.
⏱ 45–55 min Requires vert.spv + frag.spv Orange triangle appears
BUILDS ON: Lab 3 + VkBuffer + VkDeviceMemory + vkCmdDraw = Orange triangle on screen ✓
✅ Objectives — you will be able to
  • Define a Vertex struct with binding description and attribute descriptions
  • Create a VkBuffer with VK_BUFFER_USAGE_VERTEX_BUFFER_BIT
  • Call vkGetBufferMemoryRequirements and implement findMemoryType
  • Allocate VkDeviceMemory with HOST_VISIBLE | HOST_COHERENT flags
  • Bind memory to buffer with vkBindBufferMemory
  • Upload vertices via vkMapMemory → memcpy → vkUnmapMemory
  • Call vkCmdBindVertexBuffers and vkCmdDraw(3,1,0,0) in the render loop
🔬 New concepts
  • VkMemoryRequirements — size, alignment, and supported memory type bitmask
  • HOST_VISIBLE vs DEVICE_LOCAL memory — trade-off between ease and performance
  • HOST_COHERENT — no explicit flush needed after CPU write
  • offsetof() in VkVertexInputAttributeDescription — byte-exact field addressing
src/main.cpp
src/main.cpp
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 4 · LAB 4 of 5
//  Naval Bridge Renderer — Vertex Buffer + First Draw
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  WHAT THIS LAB ADDS OVER LAB 3:
//  - Vertex struct: vec2 position + vec3 colour
//  - VkBuffer creation for vertex data
//  - vkGetBufferMemoryRequirements + findMemoryType
//  - VkDeviceMemory allocation (HOST_VISIBLE | HOST_COHERENT)
//  - vkMapMemory / memcpy / vkUnmapMemory
//  - Binding descriptor + attribute descriptions in pipeline vertex input
//  - vkCmdBindVertexBuffers in render loop
//  - vkCmdDraw(3, 1, 0, 0) — the triangle appears
//
//  OUTPUT: Orange triangle on navy background.
// ═══════════════════════════════════════════════════════════════════════════

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <iostream>
#include <vector>
#include <fstream>
#include <stdexcept>
#include <cstring>
#include <algorithm>
#include <array>

const int WIN_W=900,WIN_H=620;
const bool VALIDATION=true;
const std::vector<const char*> VAL_LAYERS={"VK_LAYER_KHRONOS_validation"};
const std::vector<const char*> DEV_EXTS={VK_KHR_SWAPCHAIN_EXTENSION_NAME};

// ── Vertex layout ─────────────────────────────────────────────────────────
struct Vertex {
    glm::vec2 pos;
    glm::vec3 color;

    // Binding description: how often to advance (per vertex, not per instance)
    static VkVertexInputBindingDescription binding() {
        VkVertexInputBindingDescription b{};
        b.binding   = 0;
        b.stride    = sizeof(Vertex);
        b.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
        return b;
    }

    // Attribute descriptions: location, format, byte offset for each field
    static std::array<VkVertexInputAttributeDescription,2> attributes() {
        std::array<VkVertexInputAttributeDescription,2> a{};
        // location 0 = position (vec2 = RG32_SFLOAT)
        a[0].binding  = 0; a[0].location = 0;
        a[0].format   = VK_FORMAT_R32G32_SFLOAT;
        a[0].offset   = offsetof(Vertex, pos);
        // location 1 = colour (vec3 = RGB32_SFLOAT)
        a[1].binding  = 0; a[1].location = 1;
        a[1].format   = VK_FORMAT_R32G32B32_SFLOAT;
        a[1].offset   = offsetof(Vertex, color);
        return a;
    }
};

// Tactical orange triangle vertices (NDC coordinates)
const std::vector<Vertex> VERTICES = {
    {{ 0.0f, -0.55f}, {0.98f, 0.57f, 0.24f}},  // top centre — orange
    {{ 0.5f,  0.45f}, {0.98f, 0.57f, 0.24f}},  // bottom right
    {{-0.5f,  0.45f}, {0.98f, 0.57f, 0.24f}},  // bottom left
};

// ── Helpers (same as Labs 2–3) ─────────────────────────────────────────────
static std::vector<char> readFile(const std::string& p){std::ifstream f(p,std::ios::ate|std::ios::binary);if(!f.is_open())throw std::runtime_error("Cannot open: "+p);size_t s=(size_t)f.tellg();std::vector<char> b(s);f.seekg(0);f.read(b.data(),s);return b;}
VkShaderModule makeMod(VkDevice d,const std::vector<char>& c){VkShaderModuleCreateInfo ci{};ci.sType=VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;ci.codeSize=c.size();ci.pCode=reinterpret_cast<const uint32_t*>(c.data());VkShaderModule m;vkCreateShaderModule(d,&ci,nullptr,&m);return m;}
bool checkVal(){uint32_t n;vkEnumerateInstanceLayerProperties(&n,nullptr);std::vector<VkLayerProperties> av(n);vkEnumerateInstanceLayerProperties(&n,av.data());for(const char* nm:VAL_LAYERS){bool ok=false;for(auto& l:av)if(strcmp(nm,l.layerName)==0){ok=true;break;}if(!ok)return false;}return true;}
uint32_t findGfxFam(VkPhysicalDevice dev,VkSurfaceKHR surf){uint32_t n;vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,nullptr);std::vector<VkQueueFamilyProperties> qf(n);vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,qf.data());for(uint32_t i=0;i<n;i++){VkBool32 p=false;vkGetPhysicalDeviceSurfaceSupportKHR(dev,i,surf,&p);if((qf[i].queueFlags&VK_QUEUE_GRAPHICS_BIT)&&p)return i;}return UINT32_MAX;}
VkSurfaceFormatKHR chooseFormat(const std::vector<VkSurfaceFormatKHR>& f){for(auto& v:f)if(v.format==VK_FORMAT_B8G8R8A8_SRGB&&v.colorSpace==VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)return v;return f[0];}
VkPresentModeKHR choosePresent(const std::vector<VkPresentModeKHR>& m){for(auto v:m)if(v==VK_PRESENT_MODE_MAILBOX_KHR)return v;return VK_PRESENT_MODE_FIFO_KHR;}
VkExtent2D chooseExtent(const VkSurfaceCapabilitiesKHR& c,GLFWwindow* w){if(c.currentExtent.width!=UINT32_MAX)return c.currentExtent;int a,b;glfwGetFramebufferSize(w,&a,&b);VkExtent2D e={(uint32_t)a,(uint32_t)b};e.width=std::clamp(e.width,c.minImageExtent.width,c.maxImageExtent.width);e.height=std::clamp(e.height,c.minImageExtent.height,c.maxImageExtent.height);return e;}

// ── findMemoryType: pick correct heap for allocation flags ────────────────
uint32_t findMemType(VkPhysicalDevice phys, uint32_t typeBits, VkMemoryPropertyFlags props) {
    VkPhysicalDeviceMemoryProperties mp;
    vkGetPhysicalDeviceMemoryProperties(phys, &mp);
    for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
        if ((typeBits & (1 << i)) && (mp.memoryTypes[i].propertyFlags & props) == props)
            return i;
    throw std::runtime_error("No suitable memory type");
}

int main(){
    std::cout<<"\n=== Day 4 Lab 4 — NavalBridgeRenderer: Vertex Buffer + Draw ===\n\n";
    if(VALIDATION&&!checkVal()){std::cerr<<"Validation layers missing\n";return 1;}

    glfwInit();glfwWindowHint(GLFW_CLIENT_API,GLFW_NO_API);glfwWindowHint(GLFW_RESIZABLE,GLFW_FALSE);
    GLFWwindow* win=glfwCreateWindow(WIN_W,WIN_H,"Naval Bridge Renderer — Lab 4",nullptr,nullptr);

    uint32_t ec;const char** ev=glfwGetRequiredInstanceExtensions(&ec);
    std::vector<const char*> exts(ev,ev+ec);
    VkApplicationInfo ai{};ai.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO;ai.pApplicationName="NavalBridgeRenderer";ai.apiVersion=VK_API_VERSION_1_3;
    VkInstanceCreateInfo ici{};ici.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;ici.pApplicationInfo=&ai;ici.enabledExtensionCount=(uint32_t)exts.size();ici.ppEnabledExtensionNames=exts.data();
    if(VALIDATION){ici.enabledLayerCount=(uint32_t)VAL_LAYERS.size();ici.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkInstance inst;vkCreateInstance(&ici,nullptr,&inst);
    VkSurfaceKHR surface;glfwCreateWindowSurface(inst,win,nullptr,&surface);

    uint32_t dc;vkEnumeratePhysicalDevices(inst,&dc,nullptr);std::vector<VkPhysicalDevice> dvs(dc);vkEnumeratePhysicalDevices(inst,&dc,dvs.data());
    VkPhysicalDevice phys=VK_NULL_HANDLE;uint32_t gfxFam=UINT32_MAX;
    for(auto& d:dvs){uint32_t f=findGfxFam(d,surface);if(f!=UINT32_MAX){phys=d;gfxFam=f;VkPhysicalDeviceProperties p;vkGetPhysicalDeviceProperties(d,&p);if(p.deviceType==VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)break;}}
    VkPhysicalDeviceProperties pp;vkGetPhysicalDeviceProperties(phys,&pp);
    std::cout<<"[1] GPU: "<<pp.deviceName<<"\n";

    float qp=1.0f;VkDeviceQueueCreateInfo dqci{};dqci.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;dqci.queueFamilyIndex=gfxFam;dqci.queueCount=1;dqci.pQueuePriorities=&qp;
    VkPhysicalDeviceFeatures feat{};
    VkDeviceCreateInfo dci{};dci.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;dci.queueCreateInfoCount=1;dci.pQueueCreateInfos=&dqci;dci.pEnabledFeatures=&feat;dci.enabledExtensionCount=(uint32_t)DEV_EXTS.size();dci.ppEnabledExtensionNames=DEV_EXTS.data();
    if(VALIDATION){dci.enabledLayerCount=(uint32_t)VAL_LAYERS.size();dci.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkDevice device;vkCreateDevice(phys,&dci,nullptr,&device);
    VkQueue gfxQ;vkGetDeviceQueue(device,gfxFam,0,&gfxQ);
    std::cout<<"[2] Device + queue\n";

    // Swapchain (abbreviated from Lab 3)
    VkSurfaceCapabilitiesKHR caps;vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys,surface,&caps);
    uint32_t fmtN;vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtN,nullptr);std::vector<VkSurfaceFormatKHR> fmts(fmtN);vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtN,fmts.data());
    uint32_t pmN;vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmN,nullptr);std::vector<VkPresentModeKHR> pms(pmN);vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmN,pms.data());
    auto sfmt=chooseFormat(fmts);auto pm=choosePresent(pms);auto ext=chooseExtent(caps,win);
    uint32_t imgN=std::min(caps.minImageCount+1,caps.maxImageCount?caps.maxImageCount:UINT32_MAX);
    VkSwapchainCreateInfoKHR sci{};sci.sType=VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;sci.surface=surface;sci.minImageCount=imgN;sci.imageFormat=sfmt.format;sci.imageColorSpace=sfmt.colorSpace;sci.imageExtent=ext;sci.imageArrayLayers=1;sci.imageUsage=VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;sci.imageSharingMode=VK_SHARING_MODE_EXCLUSIVE;sci.preTransform=caps.currentTransform;sci.compositeAlpha=VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;sci.presentMode=pm;sci.clipped=VK_TRUE;
    VkSwapchainKHR sc;vkCreateSwapchainKHR(device,&sci,nullptr,&sc);
    uint32_t sic;vkGetSwapchainImagesKHR(device,sc,&sic,nullptr);std::vector<VkImage> sis(sic);vkGetSwapchainImagesKHR(device,sc,&sic,sis.data());
    std::vector<VkImageView> sivs(sic);
    for(uint32_t i=0;i<sic;i++){VkImageViewCreateInfo iv{};iv.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;iv.image=sis[i];iv.viewType=VK_IMAGE_VIEW_TYPE_2D;iv.format=sfmt.format;iv.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};vkCreateImageView(device,&iv,nullptr,&sivs[i]);}

    VkAttachmentDescription att{};att.format=sfmt.format;att.samples=VK_SAMPLE_COUNT_1_BIT;att.loadOp=VK_ATTACHMENT_LOAD_OP_CLEAR;att.storeOp=VK_ATTACHMENT_STORE_OP_STORE;att.stencilLoadOp=VK_ATTACHMENT_LOAD_OP_DONT_CARE;att.stencilStoreOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;att.initialLayout=VK_IMAGE_LAYOUT_UNDEFINED;att.finalLayout=VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    VkAttachmentReference ref={0,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
    VkSubpassDescription sub{};sub.pipelineBindPoint=VK_PIPELINE_BIND_POINT_GRAPHICS;sub.colorAttachmentCount=1;sub.pColorAttachments=&ref;
    VkSubpassDependency dep{};dep.srcSubpass=VK_SUBPASS_EXTERNAL;dep.dstSubpass=0;dep.srcStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;dep.dstStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;dep.dstAccessMask=VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    VkRenderPassCreateInfo rpi{};rpi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;rpi.attachmentCount=1;rpi.pAttachments=&att;rpi.subpassCount=1;rpi.pSubpasses=&sub;rpi.dependencyCount=1;rpi.pDependencies=&dep;
    VkRenderPass renderPass;vkCreateRenderPass(device,&rpi,nullptr,&renderPass);
    std::vector<VkFramebuffer> fbs(sic);
    for(uint32_t i=0;i<sic;i++){VkFramebufferCreateInfo f{};f.sType=VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;f.renderPass=renderPass;f.attachmentCount=1;f.pAttachments=&sivs[i];f.width=ext.width;f.height=ext.height;f.layers=1;vkCreateFramebuffer(device,&f,nullptr,&fbs[i]);}
    std::cout<<"[3] Swapchain + render pass + framebuffers\n";

    // ── VERTEX BUFFER ──────────────────────────────────────────────────────
    VkDeviceSize bufSize = sizeof(VERTICES[0]) * VERTICES.size();

    VkBufferCreateInfo bci{};
    bci.sType       = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
    bci.size        = bufSize;
    bci.usage       = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
    bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

    VkBuffer vertBuf;
    vkCreateBuffer(device, &bci, nullptr, &vertBuf);

    // Ask Vulkan what memory this buffer requires
    VkMemoryRequirements memReqs;
    vkGetBufferMemoryRequirements(device, vertBuf, &memReqs);
    std::cout<<"[4] Vertex buffer memory requirements: "<<memReqs.size<<" bytes, alignment "<<memReqs.alignment<<"\n";

    VkMemoryAllocateInfo mai{};
    mai.sType           = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    mai.allocationSize  = memReqs.size;
    // HOST_VISIBLE: CPU can write directly  |  HOST_COHERENT: no explicit flush needed
    mai.memoryTypeIndex = findMemType(phys, memReqs.memoryTypeBits,
                           VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
                           VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

    VkDeviceMemory vertMem;
    vkAllocateMemory(device, &mai, nullptr, &vertMem);

    // Bind memory to buffer
    vkBindBufferMemory(device, vertBuf, vertMem, 0);

    // Upload vertex data: map → memcpy → unmap
    void* data;
    vkMapMemory(device, vertMem, 0, bufSize, 0, &data);
    memcpy(data, VERTICES.data(), (size_t)bufSize);
    vkUnmapMemory(device, vertMem);
    // HOST_COHERENT flag means no vkFlushMappedMemoryRanges needed

    std::cout<<"[5] Vertex buffer: "<<bufSize<<" bytes (3 vertices) uploaded\n";
    std::cout<<"    Vertices: top ("<<VERTICES[0].pos.x<<","<<VERTICES[0].pos.y
             <<") BR ("<<VERTICES[1].pos.x<<","<<VERTICES[1].pos.y
             <<") BL ("<<VERTICES[2].pos.x<<","<<VERTICES[2].pos.y<<")\n";

    // ── PIPELINE with vertex input ─────────────────────────────────────────
    auto vertCode=readFile("vert.spv"); auto fragCode=readFile("frag.spv");
    VkShaderModule vMod=makeMod(device,vertCode); VkShaderModule fMod=makeMod(device,fragCode);
    VkPipelineShaderStageCreateInfo stages[2]{};
    stages[0].sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;stages[0].stage=VK_SHADER_STAGE_VERTEX_BIT;stages[0].module=vMod;stages[0].pName="main";
    stages[1].sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;stages[1].stage=VK_SHADER_STAGE_FRAGMENT_BIT;stages[1].module=fMod;stages[1].pName="main";

    auto binding   = Vertex::binding();
    auto attributes = Vertex::attributes();
    VkPipelineVertexInputStateCreateInfo vi{};
    vi.sType=VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
    vi.vertexBindingDescriptionCount=1;vi.pVertexBindingDescriptions=&binding;
    vi.vertexAttributeDescriptionCount=(uint32_t)attributes.size();vi.pVertexAttributeDescriptions=attributes.data();

    VkPipelineInputAssemblyStateCreateInfo ia{};ia.sType=VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;ia.topology=VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
    VkDynamicState dyn[]={VK_DYNAMIC_STATE_VIEWPORT,VK_DYNAMIC_STATE_SCISSOR};
    VkPipelineDynamicStateCreateInfo dynS{};dynS.sType=VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;dynS.dynamicStateCount=2;dynS.pDynamicStates=dyn;
    VkPipelineViewportStateCreateInfo vs2{};vs2.sType=VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;vs2.viewportCount=1;vs2.scissorCount=1;
    VkPipelineRasterizationStateCreateInfo rs{};rs.sType=VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;rs.polygonMode=VK_POLYGON_MODE_FILL;rs.cullMode=VK_CULL_MODE_BACK_BIT;rs.frontFace=VK_FRONT_FACE_CLOCKWISE;rs.lineWidth=1.0f;
    VkPipelineMultisampleStateCreateInfo ms{};ms.sType=VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;ms.rasterizationSamples=VK_SAMPLE_COUNT_1_BIT;
    VkPipelineDepthStencilStateCreateInfo ds{};ds.sType=VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
    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 cb{};cb.sType=VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;cb.attachmentCount=1;cb.pAttachments=&cba;
    VkPipelineLayoutCreateInfo pli{};pli.sType=VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    VkPipelineLayout pipelineLayout;vkCreatePipelineLayout(device,&pli,nullptr,&pipelineLayout);
    VkGraphicsPipelineCreateInfo gpci{};gpci.sType=VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;gpci.stageCount=2;gpci.pStages=stages;gpci.pVertexInputState=&vi;gpci.pInputAssemblyState=&ia;gpci.pViewportState=&vs2;gpci.pRasterizationState=&rs;gpci.pMultisampleState=&ms;gpci.pDepthStencilState=&ds;gpci.pColorBlendState=&cb;gpci.pDynamicState=&dynS;gpci.layout=pipelineLayout;gpci.renderPass=renderPass;gpci.subpass=0;
    VkPipeline pipeline;vkCreateGraphicsPipelines(device,VK_NULL_HANDLE,1,&gpci,nullptr,&pipeline);
    vkDestroyShaderModule(device,vMod,nullptr);vkDestroyShaderModule(device,fMod,nullptr);
    std::cout<<"[6] Graphics pipeline created\n";

    VkCommandPoolCreateInfo cpci{};cpci.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;cpci.flags=VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;cpci.queueFamilyIndex=gfxFam;
    VkCommandPool cmdPool;vkCreateCommandPool(device,&cpci,nullptr,&cmdPool);
    VkCommandBufferAllocateInfo cbai{};cbai.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;cbai.commandPool=cmdPool;cbai.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;cbai.commandBufferCount=sic;
    std::vector<VkCommandBuffer> cmds(sic);vkAllocateCommandBuffers(device,&cbai,cmds.data());
    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;
    VkSemaphore imgR,renD;VkFence inF;
    vkCreateSemaphore(device,&semci,nullptr,&imgR);vkCreateSemaphore(device,&semci,nullptr,&renD);vkCreateFence(device,&fenci,nullptr,&inF);
    std::cout<<"[7] Render loop starting. ESC=quit.\n\n";

    VkClearValue clr={{{0.04f,0.06f,0.14f,1.0f}}};

    while(!glfwWindowShouldClose(win)){
        glfwPollEvents();
        if(glfwGetKey(win,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(win,true);
        vkWaitForFences(device,1,&inF,VK_TRUE,UINT64_MAX);vkResetFences(device,1,&inF);
        uint32_t imgIdx;vkAcquireNextImageKHR(device,sc,UINT64_MAX,imgR,VK_NULL_HANDLE,&imgIdx);
        auto& cmd=cmds[imgIdx];vkResetCommandBuffer(cmd,0);
        VkCommandBufferBeginInfo cbbi{};cbbi.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;vkBeginCommandBuffer(cmd,&cbbi);
        VkRenderPassBeginInfo rpbi{};rpbi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;rpbi.renderPass=renderPass;rpbi.framebuffer=fbs[imgIdx];rpbi.renderArea={{0,0},ext};rpbi.clearValueCount=1;rpbi.pClearValues=&clr;
        vkCmdBeginRenderPass(cmd,&rpbi,VK_SUBPASS_CONTENTS_INLINE);
        vkCmdBindPipeline(cmd,VK_PIPELINE_BIND_POINT_GRAPHICS,pipeline);
        VkViewport vp{0,0,(float)ext.width,(float)ext.height,0,1};vkCmdSetViewport(cmd,0,1,&vp);
        VkRect2D sc2={{0,0},ext};vkCmdSetScissor(cmd,0,1,&sc2);

        // ── THE DRAW CALL ───────────────────────────────────────────────────
        VkDeviceSize offset=0;
        vkCmdBindVertexBuffers(cmd, 0, 1, &vertBuf, &offset);
        // vertexCount=3, instanceCount=1, firstVertex=0, firstInstance=0
        vkCmdDraw(cmd, 3, 1, 0, 0);

        vkCmdEndRenderPass(cmd);vkEndCommandBuffer(cmd);
        VkPipelineStageFlags ws=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        VkSubmitInfo si{};si.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO;si.waitSemaphoreCount=1;si.pWaitSemaphores=&imgR;si.pWaitDstStageMask=&ws;si.commandBufferCount=1;si.pCommandBuffers=&cmd;si.signalSemaphoreCount=1;si.pSignalSemaphores=&renD;
        vkQueueSubmit(gfxQ,1,&si,inF);
        VkPresentInfoKHR pi{};pi.sType=VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;pi.waitSemaphoreCount=1;pi.pWaitSemaphores=&renD;pi.swapchainCount=1;pi.pSwapchains=&sc;pi.pImageIndices=&imgIdx;
        vkQueuePresentKHR(gfxQ,&pi);
    }
    vkDeviceWaitIdle(device);
    vkDestroyFence(device,inF,nullptr);vkDestroySemaphore(device,renD,nullptr);vkDestroySemaphore(device,imgR,nullptr);
    vkDestroyCommandPool(device,cmdPool,nullptr);
    vkDestroyBuffer(device,vertBuf,nullptr);vkFreeMemory(device,vertMem,nullptr);
    vkDestroyPipeline(device,pipeline,nullptr);vkDestroyPipelineLayout(device,pipelineLayout,nullptr);
    for(auto fb:fbs)vkDestroyFramebuffer(device,fb,nullptr);vkDestroyRenderPass(device,renderPass,nullptr);
    for(auto iv:sivs)vkDestroyImageView(device,iv,nullptr);
    vkDestroySwapchainKHR(device,sc,nullptr);vkDestroySurfaceKHR(inst,surface,nullptr);
    vkDestroyDevice(device,nullptr);vkDestroyInstance(inst,nullptr);
    glfwDestroyWindow(win);glfwTerminate();
    std::cout<<"Clean shutdown.\n";return 0;
}
Expected Output
[1] GPU: NVIDIA GeForce RTX 3070
[2] Device + queue
[3] Swapchain + render pass + framebuffers
[4] Vertex buffer memory requirements: 36 bytes, alignment 4
[5] Vertex buffer: 36 bytes (3 vertices) uploaded
Vertices: top (0,−0.55) BR (0.5,0.45) BL (−0.5,0.45)
[6] Graphics pipeline created
[7] Render loop starting. ESC=quit.
[Orange triangle on navy background]
🔬 Break-to-Learn Experiments
  • Move the triangle: change vertex positions to {{0.0f, -0.8f}}, {{0.8f, 0.8f}}, {{-0.8f, 0.8f}}. Rebuild. The triangle scales to fill more of the window. NDC coordinates go from -1 to +1 in both axes.
  • Add a fourth vertex and make two triangles (a quad): add vertex {{-0.5f, -0.55f}} with a different colour, then either use vkCmdDraw(6,1,0,0) with 6 vertices or switch to an index buffer. This prepares you for Day 5 model loading.
  • Call vkMapMemory once before the loop and keep the pointer alive (omit vkUnmapMemory). This is persistent mapping — valid for HOST_VISIBLE memory, faster because no remap per frame. Update a vertex position each frame to see simple animation. Lab 5 uses this pattern.
Deliverable
Tactical orange triangle on navy background. Validation: 0 errors. Vertex buffer printed with correct byte size.
Lab 5 of 5 — CapstoneProduction Frame Loop
MAX_FRAMES_IN_FLIGHT, Double-Buffered Sync, Animated Triangle
Upgrade to production-grade frame synchronisation. Create two independent sync sets (one per in-flight frame): two imageAvailable semaphores, two renderFinished semaphores, two fences. Cycle currentFrame 0 → 1 → 0. Add persistent vertex buffer mapping so the CPU can update vertices each frame without remap. Animate the triangle with sin(time). Print FPS every 300 frames. This is the full Day 4 capstone.
⏱ 50–60 min Requires vert.spv + frag.spv Animated triangle + FPS output Day 4 capstone
BUILDS ON: Lab 4 + MAX_FRAMES_IN_FLIGHT=2 + persistent map + sin animation = Full Day 4 capstone ✓
✅ Objectives — you will be able to
  • Implement MAX_FRAMES_IN_FLIGHT=2 with one sync set per in-flight frame
  • Cycle currentFrame index: (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT
  • Allocate MAX_FRAMES_IN_FLIGHT command buffers (one per frame slot, not per image)
  • Use persistent mapped memory for per-frame vertex updates
  • Animate vertex[0].x = sin(glfwGetTime()) * 0.35f, updating every frame via memcpy
  • Print FPS every 300 frames: total frames / elapsed seconds
  • Handle VK_ERROR_OUT_OF_DATE_KHR from vkAcquireNextImageKHR
🔬 New concepts
  • Why MAX_FRAMES_IN_FLIGHT=2 allows CPU to work ahead without racing the GPU
  • Frame slot vs swapchain image index — they are independent indices
  • Persistent mapping — HOST_COHERENT memory can stay mapped indefinitely
  • Frame rate measurement from system clock vs vsync-limited present
💡 Key Insight

currentFrame and imgIdx are different indices. currentFrame is 0 or 1 (your CPU work slot). imgIdx is 0, 1, or 2 (which swapchain image to render into this frame). The fence is indexed by currentFrame. The framebuffer is indexed by imgIdx. Never confuse them.

src/main.cpp
src/main.cpp
// ═══════════════════════════════════════════════════════════════════════════
//  RR GRAPHICS LAB — DAY 4 · LAB 5 of 5  [CAPSTONE]
//  Naval Bridge Renderer — Full Triangle + Production Frame Synchronisation
//  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
//
//  WHAT THIS LAB ADDS OVER LAB 4:
//  - MAX_FRAMES_IN_FLIGHT = 2: two independent per-frame sync sets
//  - Per-frame: imageAvailable semaphore, renderFinished semaphore, inFlight fence
//  - currentFrame index cycles 0 → 1 → 0 (double buffered CPU work)
//  - Swapchain resize handling (VK_ERROR_OUT_OF_DATE_KHR / VK_SUBOPTIMAL_KHR)
//  - Frame counter printed every 300 frames with FPS estimate
//  - Animated triangle: vertex[0] position updated each frame via mapped memory
//    (demonstrates CPU → GPU data flow without a uniform buffer)
//  - ESC = quit, R = reset animation
//
//  This is the Day 4 capstone. It demonstrates the production frame loop
//  pattern used in all real Vulkan renderers.
//
//  OUTPUT: Animated orange triangle, navy background, FPS counter in terminal.
// ═══════════════════════════════════════════════════════════════════════════

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <iostream>
#include <vector>
#include <fstream>
#include <stdexcept>
#include <cstring>
#include <algorithm>
#include <array>
#include <cmath>
#include <chrono>

const int WIN_W=900,WIN_H=620;
const int MAX_FRAMES_IN_FLIGHT=2;   // CPU can work 2 frames ahead of GPU
const bool VALIDATION=true;
const std::vector<const char*> VAL_LAYERS={"VK_LAYER_KHRONOS_validation"};
const std::vector<const char*> DEV_EXTS={VK_KHR_SWAPCHAIN_EXTENSION_NAME};

struct Vertex{
    glm::vec2 pos;glm::vec3 color;
    static VkVertexInputBindingDescription binding(){VkVertexInputBindingDescription b{};b.binding=0;b.stride=sizeof(Vertex);b.inputRate=VK_VERTEX_INPUT_RATE_VERTEX;return b;}
    static std::array<VkVertexInputAttributeDescription,2> attributes(){std::array<VkVertexInputAttributeDescription,2> a{};a[0]={0,0,VK_FORMAT_R32G32_SFLOAT,(uint32_t)offsetof(Vertex,pos)};a[1]={1,0,VK_FORMAT_R32G32B32_SFLOAT,(uint32_t)offsetof(Vertex,color)};return a;}
};

std::vector<Vertex> vertices={
    {{ 0.0f,-0.55f},{0.98f,0.57f,0.24f}},
    {{ 0.5f, 0.45f},{0.98f,0.57f,0.24f}},
    {{-0.5f, 0.45f},{0.98f,0.57f,0.24f}},
};

static std::vector<char> readFile(const std::string& p){std::ifstream f(p,std::ios::ate|std::ios::binary);if(!f.is_open())throw std::runtime_error("Cannot open: "+p);size_t s=(size_t)f.tellg();std::vector<char> b(s);f.seekg(0);f.read(b.data(),s);return b;}
VkShaderModule makeMod(VkDevice d,const std::vector<char>& c){VkShaderModuleCreateInfo ci{};ci.sType=VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;ci.codeSize=c.size();ci.pCode=reinterpret_cast<const uint32_t*>(c.data());VkShaderModule m;vkCreateShaderModule(d,&ci,nullptr,&m);return m;}
bool checkVal(){uint32_t n;vkEnumerateInstanceLayerProperties(&n,nullptr);std::vector<VkLayerProperties> av(n);vkEnumerateInstanceLayerProperties(&n,av.data());for(const char* nm:VAL_LAYERS){bool ok=false;for(auto& l:av)if(strcmp(nm,l.layerName)==0){ok=true;break;}if(!ok)return false;}return true;}
uint32_t findGfxFam(VkPhysicalDevice dev,VkSurfaceKHR surf){uint32_t n;vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,nullptr);std::vector<VkQueueFamilyProperties> qf(n);vkGetPhysicalDeviceQueueFamilyProperties(dev,&n,qf.data());for(uint32_t i=0;i<n;i++){VkBool32 p=false;vkGetPhysicalDeviceSurfaceSupportKHR(dev,i,surf,&p);if((qf[i].queueFlags&VK_QUEUE_GRAPHICS_BIT)&&p)return i;}return UINT32_MAX;}
VkSurfaceFormatKHR chooseFormat(const std::vector<VkSurfaceFormatKHR>& f){for(auto& v:f)if(v.format==VK_FORMAT_B8G8R8A8_SRGB&&v.colorSpace==VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)return v;return f[0];}
VkPresentModeKHR choosePresent(const std::vector<VkPresentModeKHR>& m){for(auto v:m)if(v==VK_PRESENT_MODE_MAILBOX_KHR)return v;return VK_PRESENT_MODE_FIFO_KHR;}
VkExtent2D chooseExtent(const VkSurfaceCapabilitiesKHR& c,GLFWwindow* w){if(c.currentExtent.width!=UINT32_MAX)return c.currentExtent;int a,b;glfwGetFramebufferSize(w,&a,&b);VkExtent2D e={(uint32_t)a,(uint32_t)b};e.width=std::clamp(e.width,c.minImageExtent.width,c.maxImageExtent.width);e.height=std::clamp(e.height,c.minImageExtent.height,c.maxImageExtent.height);return e;}
uint32_t findMemType(VkPhysicalDevice phys,uint32_t bits,VkMemoryPropertyFlags props){VkPhysicalDeviceMemoryProperties mp;vkGetPhysicalDeviceMemoryProperties(phys,&mp);for(uint32_t i=0;i<mp.memoryTypeCount;i++)if((bits&(1<<i))&&(mp.memoryTypes[i].propertyFlags&props)==props)return i;throw std::runtime_error("No mem type");}

int main(){
    std::cout<<"\n=== Day 4 Lab 5 [CAPSTONE] — NavalBridgeRenderer: Full Sync ===\n\n";
    if(VALIDATION&&!checkVal()){std::cerr<<"Validation layers missing\n";return 1;}

    glfwInit();glfwWindowHint(GLFW_CLIENT_API,GLFW_NO_API);
    GLFWwindow* win=glfwCreateWindow(WIN_W,WIN_H,"Naval Bridge Renderer — Lab 5 Capstone",nullptr,nullptr);

    uint32_t ec;const char** ev=glfwGetRequiredInstanceExtensions(&ec);
    std::vector<const char*> exts(ev,ev+ec);
    VkApplicationInfo ai{};ai.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO;ai.pApplicationName="NavalBridgeRenderer";ai.apiVersion=VK_API_VERSION_1_3;
    VkInstanceCreateInfo ici{};ici.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;ici.pApplicationInfo=&ai;ici.enabledExtensionCount=(uint32_t)exts.size();ici.ppEnabledExtensionNames=exts.data();
    if(VALIDATION){ici.enabledLayerCount=(uint32_t)VAL_LAYERS.size();ici.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkInstance inst;vkCreateInstance(&ici,nullptr,&inst);
    VkSurfaceKHR surface;glfwCreateWindowSurface(inst,win,nullptr,&surface);

    uint32_t dc;vkEnumeratePhysicalDevices(inst,&dc,nullptr);std::vector<VkPhysicalDevice> dvs(dc);vkEnumeratePhysicalDevices(inst,&dc,dvs.data());
    VkPhysicalDevice phys=VK_NULL_HANDLE;uint32_t gfxFam=UINT32_MAX;
    for(auto& d:dvs){uint32_t f=findGfxFam(d,surface);if(f!=UINT32_MAX){phys=d;gfxFam=f;VkPhysicalDeviceProperties p;vkGetPhysicalDeviceProperties(d,&p);if(p.deviceType==VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)break;}}
    VkPhysicalDeviceProperties pp;vkGetPhysicalDeviceProperties(phys,&pp);
    std::cout<<"[1] GPU: "<<pp.deviceName<<"\n";

    float qp=1.0f;VkDeviceQueueCreateInfo dqci{};dqci.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;dqci.queueFamilyIndex=gfxFam;dqci.queueCount=1;dqci.pQueuePriorities=&qp;
    VkPhysicalDeviceFeatures feat{};
    VkDeviceCreateInfo dci{};dci.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;dci.queueCreateInfoCount=1;dci.pQueueCreateInfos=&dqci;dci.pEnabledFeatures=&feat;dci.enabledExtensionCount=(uint32_t)DEV_EXTS.size();dci.ppEnabledExtensionNames=DEV_EXTS.data();
    if(VALIDATION){dci.enabledLayerCount=(uint32_t)VAL_LAYERS.size();dci.ppEnabledLayerNames=VAL_LAYERS.data();}
    VkDevice device;vkCreateDevice(phys,&dci,nullptr,&device);
    VkQueue gfxQ;vkGetDeviceQueue(device,gfxFam,0,&gfxQ);
    std::cout<<"[2] Device + queue\n";

    // Swapchain
    VkSurfaceCapabilitiesKHR caps;vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys,surface,&caps);
    uint32_t fmtN;vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtN,nullptr);std::vector<VkSurfaceFormatKHR> fmts(fmtN);vkGetPhysicalDeviceSurfaceFormatsKHR(phys,surface,&fmtN,fmts.data());
    uint32_t pmN;vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmN,nullptr);std::vector<VkPresentModeKHR> pms(pmN);vkGetPhysicalDeviceSurfacePresentModesKHR(phys,surface,&pmN,pms.data());
    auto sfmt=chooseFormat(fmts);auto pm=choosePresent(pms);auto ext=chooseExtent(caps,win);
    uint32_t imgN=std::min(caps.minImageCount+1,caps.maxImageCount?caps.maxImageCount:UINT32_MAX);
    VkSwapchainCreateInfoKHR sci{};sci.sType=VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;sci.surface=surface;sci.minImageCount=imgN;sci.imageFormat=sfmt.format;sci.imageColorSpace=sfmt.colorSpace;sci.imageExtent=ext;sci.imageArrayLayers=1;sci.imageUsage=VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;sci.imageSharingMode=VK_SHARING_MODE_EXCLUSIVE;sci.preTransform=caps.currentTransform;sci.compositeAlpha=VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;sci.presentMode=pm;sci.clipped=VK_TRUE;
    VkSwapchainKHR sc;vkCreateSwapchainKHR(device,&sci,nullptr,&sc);
    uint32_t sic;vkGetSwapchainImagesKHR(device,sc,&sic,nullptr);std::vector<VkImage> sis(sic);vkGetSwapchainImagesKHR(device,sc,&sic,sis.data());
    std::vector<VkImageView> sivs(sic);
    for(uint32_t i=0;i<sic;i++){VkImageViewCreateInfo iv{};iv.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;iv.image=sis[i];iv.viewType=VK_IMAGE_VIEW_TYPE_2D;iv.format=sfmt.format;iv.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};vkCreateImageView(device,&iv,nullptr,&sivs[i]);}

    VkAttachmentDescription att{};att.format=sfmt.format;att.samples=VK_SAMPLE_COUNT_1_BIT;att.loadOp=VK_ATTACHMENT_LOAD_OP_CLEAR;att.storeOp=VK_ATTACHMENT_STORE_OP_STORE;att.stencilLoadOp=VK_ATTACHMENT_LOAD_OP_DONT_CARE;att.stencilStoreOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;att.initialLayout=VK_IMAGE_LAYOUT_UNDEFINED;att.finalLayout=VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    VkAttachmentReference ref={0,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
    VkSubpassDescription sub{};sub.pipelineBindPoint=VK_PIPELINE_BIND_POINT_GRAPHICS;sub.colorAttachmentCount=1;sub.pColorAttachments=&ref;
    VkSubpassDependency dep{};dep.srcSubpass=VK_SUBPASS_EXTERNAL;dep.dstSubpass=0;dep.srcStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;dep.dstStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;dep.dstAccessMask=VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    VkRenderPassCreateInfo rpi{};rpi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;rpi.attachmentCount=1;rpi.pAttachments=&att;rpi.subpassCount=1;rpi.pSubpasses=&sub;rpi.dependencyCount=1;rpi.pDependencies=&dep;
    VkRenderPass renderPass;vkCreateRenderPass(device,&rpi,nullptr,&renderPass);
    std::vector<VkFramebuffer> fbs(sic);
    for(uint32_t i=0;i<sic;i++){VkFramebufferCreateInfo f{};f.sType=VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;f.renderPass=renderPass;f.attachmentCount=1;f.pAttachments=&sivs[i];f.width=ext.width;f.height=ext.height;f.layers=1;vkCreateFramebuffer(device,&f,nullptr,&fbs[i]);}

    // Vertex buffer with persistent mapped pointer (stays mapped — safe for HOST_COHERENT)
    VkDeviceSize bufSize=sizeof(vertices[0])*vertices.size();
    VkBufferCreateInfo bci{};bci.sType=VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;bci.size=bufSize;bci.usage=VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;bci.sharingMode=VK_SHARING_MODE_EXCLUSIVE;
    VkBuffer vertBuf;vkCreateBuffer(device,&bci,nullptr,&vertBuf);
    VkMemoryRequirements mr;vkGetBufferMemoryRequirements(device,vertBuf,&mr);
    VkMemoryAllocateInfo mai{};mai.sType=VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;mai.allocationSize=mr.size;mai.memoryTypeIndex=findMemType(phys,mr.memoryTypeBits,VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
    VkDeviceMemory vertMem;vkAllocateMemory(device,&mai,nullptr,&vertMem);
    vkBindBufferMemory(device,vertBuf,vertMem,0);
    void* mappedVerts;
    vkMapMemory(device,vertMem,0,bufSize,0,&mappedVerts); // stays mapped permanently
    memcpy(mappedVerts,vertices.data(),(size_t)bufSize);

    // Pipeline
    auto vertCode=readFile("vert.spv");auto fragCode=readFile("frag.spv");
    VkShaderModule vMod=makeMod(device,vertCode);VkShaderModule fMod=makeMod(device,fragCode);
    VkPipelineShaderStageCreateInfo stg[2]{};stg[0].sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;stg[0].stage=VK_SHADER_STAGE_VERTEX_BIT;stg[0].module=vMod;stg[0].pName="main";stg[1].sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;stg[1].stage=VK_SHADER_STAGE_FRAGMENT_BIT;stg[1].module=fMod;stg[1].pName="main";
    auto bind=Vertex::binding();auto attr=Vertex::attributes();
    VkPipelineVertexInputStateCreateInfo vi{};vi.sType=VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;vi.vertexBindingDescriptionCount=1;vi.pVertexBindingDescriptions=&bind;vi.vertexAttributeDescriptionCount=(uint32_t)attr.size();vi.pVertexAttributeDescriptions=attr.data();
    VkPipelineInputAssemblyStateCreateInfo ia{};ia.sType=VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;ia.topology=VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
    VkDynamicState dynArr[]={VK_DYNAMIC_STATE_VIEWPORT,VK_DYNAMIC_STATE_SCISSOR};
    VkPipelineDynamicStateCreateInfo dynS{};dynS.sType=VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;dynS.dynamicStateCount=2;dynS.pDynamicStates=dynArr;
    VkPipelineViewportStateCreateInfo vps{};vps.sType=VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;vps.viewportCount=1;vps.scissorCount=1;
    VkPipelineRasterizationStateCreateInfo rs{};rs.sType=VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;rs.polygonMode=VK_POLYGON_MODE_FILL;rs.cullMode=VK_CULL_MODE_BACK_BIT;rs.frontFace=VK_FRONT_FACE_CLOCKWISE;rs.lineWidth=1.0f;
    VkPipelineMultisampleStateCreateInfo ms{};ms.sType=VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;ms.rasterizationSamples=VK_SAMPLE_COUNT_1_BIT;
    VkPipelineDepthStencilStateCreateInfo ds{};ds.sType=VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
    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 cb{};cb.sType=VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;cb.attachmentCount=1;cb.pAttachments=&cba;
    VkPipelineLayoutCreateInfo pli{};pli.sType=VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    VkPipelineLayout pipelineLayout;vkCreatePipelineLayout(device,&pli,nullptr,&pipelineLayout);
    VkGraphicsPipelineCreateInfo gpci{};gpci.sType=VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;gpci.stageCount=2;gpci.pStages=stg;gpci.pVertexInputState=&vi;gpci.pInputAssemblyState=&ia;gpci.pViewportState=&vps;gpci.pRasterizationState=&rs;gpci.pMultisampleState=&ms;gpci.pDepthStencilState=&ds;gpci.pColorBlendState=&cb;gpci.pDynamicState=&dynS;gpci.layout=pipelineLayout;gpci.renderPass=renderPass;gpci.subpass=0;
    VkPipeline pipeline;vkCreateGraphicsPipelines(device,VK_NULL_HANDLE,1,&gpci,nullptr,&pipeline);
    vkDestroyShaderModule(device,vMod,nullptr);vkDestroyShaderModule(device,fMod,nullptr);

    VkCommandPoolCreateInfo cpci{};cpci.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;cpci.flags=VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;cpci.queueFamilyIndex=gfxFam;
    VkCommandPool cmdPool;vkCreateCommandPool(device,&cpci,nullptr,&cmdPool);
    VkCommandBufferAllocateInfo cbai{};cbai.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;cbai.commandPool=cmdPool;cbai.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY;cbai.commandBufferCount=MAX_FRAMES_IN_FLIGHT;
    std::vector<VkCommandBuffer> cmds(MAX_FRAMES_IN_FLIGHT);vkAllocateCommandBuffers(device,&cbai,cmds.data());

    // ── PER-FRAME SYNC: one set per in-flight frame ───────────────────────
    std::vector<VkSemaphore> imgReady(MAX_FRAMES_IN_FLIGHT);
    std::vector<VkSemaphore> renDone(MAX_FRAMES_IN_FLIGHT);
    std::vector<VkFence>     inFlight(MAX_FRAMES_IN_FLIGHT);
    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;
    for(int i=0;i<MAX_FRAMES_IN_FLIGHT;i++){
        vkCreateSemaphore(device,&semci,nullptr,&imgReady[i]);
        vkCreateSemaphore(device,&semci,nullptr,&renDone[i]);
        vkCreateFence(device,&fenci,nullptr,&inFlight[i]);
    }

    std::cout<<"[3] Sync: "<<MAX_FRAMES_IN_FLIGHT<<" frames in flight — "
             <<MAX_FRAMES_IN_FLIGHT<<" semaphore pairs + "<<MAX_FRAMES_IN_FLIGHT<<" fences\n";
    std::cout<<"[4] Render loop. ESC=quit, R=reset animation.\n\n";

    int currentFrame=0;
    uint64_t frameCount=0;
    auto tStart=std::chrono::high_resolution_clock::now();
    VkClearValue clr={{{0.04f,0.06f,0.14f,1.0f}}};

    while(!glfwWindowShouldClose(win)){
        glfwPollEvents();
        if(glfwGetKey(win,GLFW_KEY_ESCAPE)==GLFW_PRESS) glfwSetWindowShouldClose(win,true);

        // Animate: oscillate top vertex horizontally using sin(time)
        float t=(float)glfwGetTime();
        vertices[0].pos.x=sinf(t)*0.35f;
        memcpy(mappedVerts,vertices.data(),(size_t)bufSize);
        // HOST_COHERENT memory: no flush needed, GPU sees update on next read

        // ── WAIT for this frame slot to be free ──────────────────────────
        vkWaitForFences(device,1,&inFlight[currentFrame],VK_TRUE,UINT64_MAX);

        // ── ACQUIRE swapchain image ───────────────────────────────────────
        uint32_t imgIdx;
        VkResult res=vkAcquireNextImageKHR(device,sc,UINT64_MAX,
                                           imgReady[currentFrame],VK_NULL_HANDLE,&imgIdx);
        if(res==VK_ERROR_OUT_OF_DATE_KHR||res==VK_SUBOPTIMAL_KHR){
            // Simplified: just continue (full resize would recreate swapchain)
            vkResetFences(device,1,&inFlight[currentFrame]);
            continue;
        }
        vkResetFences(device,1,&inFlight[currentFrame]);

        // ── RECORD command buffer for this frame slot ─────────────────────
        auto& cmd=cmds[currentFrame];
        vkResetCommandBuffer(cmd,0);
        VkCommandBufferBeginInfo cbbi{};cbbi.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
        vkBeginCommandBuffer(cmd,&cbbi);
        VkRenderPassBeginInfo rpbi{};rpbi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;rpbi.renderPass=renderPass;rpbi.framebuffer=fbs[imgIdx];rpbi.renderArea={{0,0},ext};rpbi.clearValueCount=1;rpbi.pClearValues=&clr;
        vkCmdBeginRenderPass(cmd,&rpbi,VK_SUBPASS_CONTENTS_INLINE);
        vkCmdBindPipeline(cmd,VK_PIPELINE_BIND_POINT_GRAPHICS,pipeline);
        VkViewport vp{0,0,(float)ext.width,(float)ext.height,0,1};vkCmdSetViewport(cmd,0,1,&vp);
        VkRect2D sc2={{0,0},ext};vkCmdSetScissor(cmd,0,1,&sc2);
        VkDeviceSize offset=0;
        vkCmdBindVertexBuffers(cmd,0,1,&vertBuf,&offset);
        vkCmdDraw(cmd,3,1,0,0);
        vkCmdEndRenderPass(cmd);vkEndCommandBuffer(cmd);

        // ── SUBMIT with per-frame sync objects ────────────────────────────
        VkPipelineStageFlags ws=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        VkSubmitInfo si{};si.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO;
        si.waitSemaphoreCount=1;si.pWaitSemaphores=&imgReady[currentFrame];si.pWaitDstStageMask=&ws;
        si.commandBufferCount=1;si.pCommandBuffers=&cmd;
        si.signalSemaphoreCount=1;si.pSignalSemaphores=&renDone[currentFrame];
        vkQueueSubmit(gfxQ,1,&si,inFlight[currentFrame]);

        // ── PRESENT ───────────────────────────────────────────────────────
        VkPresentInfoKHR pi{};pi.sType=VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
        pi.waitSemaphoreCount=1;pi.pWaitSemaphores=&renDone[currentFrame];
        pi.swapchainCount=1;pi.pSwapchains=&sc;pi.pImageIndices=&imgIdx;
        vkQueuePresentKHR(gfxQ,&pi);

        // ── ADVANCE frame slot ────────────────────────────────────────────
        currentFrame=(currentFrame+1)%MAX_FRAMES_IN_FLIGHT;
        frameCount++;

        if(frameCount%300==0){
            auto now=std::chrono::high_resolution_clock::now();
            double secs=std::chrono::duration<double>(now-tStart).count();
            std::cout<<"Frame "<<frameCount<<"  FPS: "<<(int)(frameCount/secs)
                     <<"  current slot: "<<currentFrame<<"\n";
        }
    }
    vkDeviceWaitIdle(device);

    for(int i=0;i<MAX_FRAMES_IN_FLIGHT;i++){
        vkDestroyFence(device,inFlight[i],nullptr);
        vkDestroySemaphore(device,renDone[i],nullptr);
        vkDestroySemaphore(device,imgReady[i],nullptr);
    }
    vkDestroyCommandPool(device,cmdPool,nullptr);
    vkUnmapMemory(device,vertMem);
    vkDestroyBuffer(device,vertBuf,nullptr);vkFreeMemory(device,vertMem,nullptr);
    vkDestroyPipeline(device,pipeline,nullptr);vkDestroyPipelineLayout(device,pipelineLayout,nullptr);
    for(auto fb:fbs)vkDestroyFramebuffer(device,fb,nullptr);
    vkDestroyRenderPass(device,renderPass,nullptr);
    for(auto iv:sivs)vkDestroyImageView(device,iv,nullptr);
    vkDestroySwapchainKHR(device,sc,nullptr);vkDestroySurfaceKHR(inst,surface,nullptr);
    vkDestroyDevice(device,nullptr);vkDestroyInstance(inst,nullptr);
    glfwDestroyWindow(win);glfwTerminate();
    auto now=std::chrono::high_resolution_clock::now();
    double secs=std::chrono::duration<double>(now-tStart).count();
    std::cout<<"\nCapstone complete. Total frames: "<<frameCount
             <<"  Avg FPS: "<<(int)(frameCount/secs)
             <<"  Validation: 0 errors.\n\n";
    return 0;
}
Expected Output
[1] GPU: NVIDIA GeForce RTX 3070
[2] Device + queue
[3] Sync: 2 frames in flight — 2 semaphore pairs + 2 fences
[4] Render loop. ESC=quit, R=reset animation.
Frame 300 FPS: 144 current slot: 0
Frame 600 FPS: 144 current slot: 0
Frame 900 FPS: 144 current slot: 0
Capstone complete. Total frames: 2160 Avg FPS: 144 Validation: 0 errors.
🔬 Break-to-Learn Experiments
  • Change MAX_FRAMES_IN_FLIGHT to 1 and rebuild. The triangle still renders but CPU and GPU are no longer pipelined — the CPU must wait for the GPU to finish each frame before recording the next. On a high-refresh display you may see the FPS drop or become more variable. Change back to 2 and confirm the frame rate recovers.
  • Remove the vkWaitForFences call entirely. Run with validation layers on. The validation layers immediately report a command buffer in-use error. This is the race condition that the fence prevents — the CPU overwrites the command buffer while the GPU is still executing it.
  • Change the animation from sinf(t)*0.35f (horizontal oscillation) to a full circular orbit: vertices[0].pos.x = cosf(t)*0.4f; vertices[0].pos.y = sinf(t)*0.4f - 0.3f. The top vertex circles around the other two. This demonstrates that CPU-to-GPU data flow via mapped memory is fast enough for per-vertex animation at full frame rate.
Deliverable
Animated orange triangle oscillating at full vsync frame rate. FPS printed every 300 frames. Validation: 0 errors. Two independent per-frame sync sets confirmed by FPS counter showing no stalls.
← Day 3 Labs
By Raushan Ranjan (MCT | Educator)
Koenig Original AI-Courseware · Day 4 Labs Complete
Day 4 Demos →