🎉Nice work!
Keep going.
Capstone Project · Week 1 Finale

Tactical Radar
OpenGLVulkan

Build a real-time 3D tactical radar display — first as a working OpenGL prototype, then rebuild it in Vulkan. Every concept from the week crystallises into one coherent application.

RR
Building with Raushan Ranjan
MCT · RR Skillverse
2Complete programmes
6Build stages
30+Checkpoints
1Week of learning = this
Overview

What We're Building

A Tactical Radar Display — the kind you'd see in a naval operations room. A circular scope, a rotating sweep line leaving a green phosphor trail, and blip contacts that fade in and out. Real-time, animated, hardware-accelerated.

Tactical Radar Display preview RNG: 50NM HDG: 045° CONTACTS: 4 MODE: AIR RR SKILLVERSE

What you will have running by the end of Phase 1

Features You Will Build

Range Rings
Concentric circles generated from a vertex loop
Sweep Line
Real-time rotation using a uniform / push constant
🌿
Phosphor Trail
Alpha fade using blending — classic CRT look
🔵
Blip Contacts
Point data rendered as instanced or individual draws
📟
HUD Text
Bitmap font overlay with range and heading data
Vulkan Rebuild
Same visual, explicit GPU control, zero driver guessing
Concepts from this week you will use
VAO/VBOGLSL shadersglUniform Alpha blendingRender loop VkPipelinePush constantsVkBuffer Render passSwapchainSemaphores GLSL 450NDC coordsVertex shaders CMakeglslcGLFW
Before You Code · Mindset

How to Think About This Project

The most important skill in graphics programming is not knowing the API — it is knowing how to decompose a visual into geometry, attributes, and draw calls. Read this section before writing a single line.

The Decomposition Rule

Every graphics programme starts with one question: what shapes do I need, and what data describes each shape? The radar display is complex visually but decomposes simply:

🧠 How a graphics programmer looks at the radar

Four separate "things" to draw: (1) The ring geometry — a circle approximated by 360 line segments, repeated 4 times at different radii. (2) The sweep line — a single line from center to edge, rotated by the current angle. (3) The blip contacts — points or small circles at known positions. (4) The HUD text — a textured quad per character OR a simple vertex-coloured overlay. Each "thing" is its own VBO, its own draw call, possibly its own shader.

The Right Order to Build

Always build in this order: get something visible first, then add detail. A programmer who tries to build the whole radar at once writes 400 lines and sees a black screen. A programmer who follows this order sees progress every 20 minutes:

1
One ring on screen
Just a white circle. No colour, no animation, no blending. Confirm: vertex data is correct, shader is correct, draw call is correct.
2
All four rings + colour
Add the remaining rings. Apply the green phosphor colour in the fragment shader. Confirm: the rings look right.
3
The sweep line moving
Add the sweep line as a second draw call. Pass the current angle as a uniform. Confirm: it rotates. Now you have animation.
4
Blip contacts
Add a few hardcoded blip positions as points or small circles. Confirm: they appear at the right positions in NDC.
5
The trail fade
Enable alpha blending. Render the sweep wedge with low alpha. The phosphor trail effect emerges from physics of the frame buffer, not from magic.
📌 The Hidden Rule of This Project

Every time you complete a stage and it works — stop. Look at the screen. Understand why it works before moving to the next stage. This reflection is what converts working code into transferable understanding. A checklist cannot force this — but your professional instinct should.

NDC to Radar Coordinates

The GPU thinks in NDC: X and Y from −1 to +1, center at (0,0). The radar scope is a circle of radius ~0.9 (leaving a small margin). A ring at range R (where R is 0.0 to 1.0) is a circle of radius R * 0.9 in NDC. A blip at polar position (angle θ, range r) has NDC coordinates:

Converting polar blip position to NDCGLSL math
// Blip at bearing theta (radians, 0=North=up) and range r (0..1)
// Radar radius in NDC = 0.9
float radar_r = r * 0.9f;
float ndc_x   = radar_r * sin(theta);  // East = +X in NDC
float ndc_y   = radar_r * cos(theta);  // North = +Y in NDC (OpenGL convention)
// In Vulkan: ndc_y = -radar_r * cos(theta) (Y axis is flipped)
✅ Confirm this before writing any code

Draw this on paper first. Mark (0,0) at center. Mark NDC (+1,0) = East, (0,+1) = North. Mark where a blip at bearing 045° range 50% would land on the circle. Now you know your coordinate system and no code will surprise you.

Phase 1 · Stage 1 — OpenGL

Folder & Build Setup

Create the project skeleton in under 5 minutes. Every file has a defined purpose. You will never wonder where a file goes.

Create the Folder Structure — Exact Commands

Open a Developer Command Prompt for VS 2022 (not regular cmd). Run:

Create all folders and files in one blockShell
mkdir C:\Labs\Capstone\RadarGL
cd C:\Labs\Capstone\RadarGL
mkdir src shaders

# Create empty files you will fill in the following stages
type nul > CMakeLists.txt
type nul > src\main.cpp
type nul > shaders\radar.vert
type nul > shaders\radar.frag

Your folder now looks like this. Every file below is the complete content — no missing pieces:

C:\Labs\Capstone\RadarGL\
├── CMakeLists.txt ← build config (fill next)
├── src\
│ └── main.cpp ← all C++ code lives here
└── shaders\
├── radar.vert ← vertex shader (GLSL)
└── radar.frag ← fragment shader (GLSL)

CMakeLists.txt — Complete, Copy Exactly

CMakeLists.txtCMake
cmake_minimum_required(VERSION 3.20)
project(RadarGL)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# GLEW — OpenGL function loader
set(GLEW_DIR $ENV{GLEW_DIR})
include_directories(${GLEW_DIR}/include)

# GLFW — window and input
set(GLFW_DIR $ENV{GLFW_DIR})
include_directories(${GLFW_DIR}/include)

# GLM — math (header only, no linking needed)
set(GLM_DIR $ENV{GLM_DIR})
include_directories(${GLM_DIR})

add_executable(RadarGL src/main.cpp)

target_link_libraries(RadarGL
    ${GLEW_DIR}/lib/Release/x64/glew32.lib
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
    opengl32.lib
)

# Copy GLEW dll next to the exe automatically
add_custom_command(TARGET RadarGL POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        "${GLEW_DIR}/bin/Release/x64/glew32.dll"
        "$<TARGET_FILE_DIR:RadarGL>"
)
⚠️ Environment Variables Must Exist

The CMakeLists.txt reads GLEW_DIR, GLFW_DIR, and GLM_DIR from your system environment. From Day 1–3 you already have GLFW_DIR set. If GLEW_DIR or GLM_DIR are missing: System Properties → Environment Variables → New. Then restart your command prompt. GLEW_DIR points to your GLEW root, GLM_DIR points to the glm/ header folder.

Configure CMake — Run This Now

Configure only (run once — do NOT build until Stage 2)Shell
cd C:\Labs\Capstone\RadarGL

# Generate Visual Studio project files — run ONCE, here in Stage 1
cmake -B build -G "Visual Studio 17 2022" -A x64

# You should see: "Build files have been written to: ...build"
# Do NOT build yet — src\main.cpp is still empty.
# Build + Run happens in Stage 2 after you write the C++ code.

Stage 1 Checkpoints

Folders created: src\ and shaders\ exist
Verify with: dir C:\Labs\Capstone\RadarGL
build
cmake -B build completes without errors
You should see "Build files have been written to: ...build"
build
Phase 1 · Stage 2 — OpenGL

The Radar Rings

Get something visible immediately. A ring is just a polygon with enough sides that it looks round. 360 vertices = a perfect circle. Write the shaders first, then the C++.

🧠 Before you type — think about this

A ring is a set of N points on a circle of radius R, connected as a line loop. The vertex shader receives each point's (x,y) position in NDC and passes it through to gl_Position. The fragment shader outputs a solid green colour. That's all the shaders do at this stage — no uniforms, no interpolation, no tricks. Keep it minimal until it works.

The Shaders — Create These First

shaders/radar.vert — paste this exactlyGLSL
#version 330 core

layout(location = 0) in vec2 inPos;   // NDC position of each vertex

uniform float uAngle;    // sweep angle in radians — used by sweep line draw
uniform float uAlpha;    // per-object opacity for trail fade

void main() {
    gl_Position = vec4(inPos, 0.0, 1.0);
    // No transform matrix needed — everything is already in NDC.
    // The radar circle fills the viewport, centered at (0,0).
}
shaders/radar.frag — paste this exactlyGLSL
#version 330 core

uniform float uAlpha;           // 0.0 = fully transparent, 1.0 = fully opaque
uniform vec3  uColour;          // RGB colour to draw this geometry

out vec4 fragColour;

void main() {
    fragColour = vec4(uColour, uAlpha);
    // Alpha is controlled by C++ per draw call.
    // The trail effect comes from drawing with low alpha over a dark background.
}

main.cpp — Stage 2: Rings Only

Write src/main.cpp with this complete content. Read every comment — they explain why, not just what.

src/main.cpp — Rings only (working programme)C++
// ═══════════════════════════════════════════════════════════════════
// RadarGL — Capstone Project · RR Skillverse
// Stage 2: Four concentric range rings
// ═══════════════════════════════════════════════════════════════════
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <cmath>

const int   WIN_W  = 900, WIN_H = 900;
const float PI     = 3.14159265358979f;
const float RADAR_R = 0.88f;  // radar circle radius in NDC
const int   SEGMENTS = 360;   // vertices per ring — more = smoother circle

// ── Load shader source from file ──────────────────────────────────────
std::string loadFile(const std::string& path) {
    std::ifstream f(path);
    if (!f.is_open()) {
        std::cerr << "ERROR: Cannot open " << path << "\n";
        exit(1);
    }
    std::ostringstream ss; ss << f.rdbuf(); return ss.str();
}

// ── Compile + link a vertex/fragment shader pair ──────────────────────
GLuint makeProgram(const std::string& vsPath, const std::string& fsPath) {
    auto vsSrc = loadFile(vsPath), fsSrc = loadFile(fsPath);
    const char* vsC = vsSrc.c_str(), *fsC = fsSrc.c_str();

    GLuint vs = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vs, 1, &vsC, nullptr); glCompileShader(vs);
    GLint ok; glGetShaderiv(vs, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[512]; glGetShaderInfoLog(vs, 512, nullptr, log);
        std::cerr << "VERT ERROR:\n" << log; exit(1);
    }
    GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fs, 1, &fsC, nullptr); glCompileShader(fs);
    glGetShaderiv(fs, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[512]; glGetShaderInfoLog(fs, 512, nullptr, log);
        std::cerr << "FRAG ERROR:\n" << log; exit(1);
    }
    GLuint prog = glCreateProgram();
    glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog);
    glDeleteShader(vs); glDeleteShader(fs);
    return prog;
}

// ── Generate circle vertices (line loop) ─────────────────────────────
// Returns a vector of 2D NDC points forming a circle of given radius
std::vector<glm::vec2> makeCircle(float radius, int segs) {
    std::vector<glm::vec2> pts;
    for (int i = 0; i <= segs; i++) {
        float a = (float)i / segs * 2.0f * PI;
        pts.push_back({ radius * cos(a), radius * sin(a) });
    }
    return pts;
}

// ── Upload geometry to a VAO/VBO pair, return VAO id ─────────────────
GLuint makeVAO(const std::vector<glm::vec2>& pts) {
    GLuint vao, vbo;
    glGenVertexArrays(1, &vao); glBindVertexArray(vao);
    glGenBuffers(1, &vbo);      glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, pts.size() * sizeof(glm::vec2),
                 pts.data(), GL_STATIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,
                          sizeof(glm::vec2), (void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
    return vao;
}

// ── Helper: set shader uniforms ───────────────────────────────────────
void setColour(GLuint prog, glm::vec3 col, float alpha) {
    glUniform3fv(glGetUniformLocation(prog, "uColour"), 1, glm::value_ptr(col));
    glUniform1f(glGetUniformLocation(prog,  "uAlpha"), alpha);
}

int main() {
    // ── Init GLFW ─────────────────────────────────────────────────────
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    GLFWwindow* win = glfwCreateWindow(WIN_W, WIN_H,
        "Tactical Radar — RR Skillverse", nullptr, nullptr);
    glfwMakeContextCurrent(win);
    glewExperimental = GL_TRUE; glewInit();

    // ── Shader programme ──────────────────────────────────────────────
    GLuint prog = makeProgram("shaders/radar.vert", "shaders/radar.frag");

    // ── Build four range rings at 25%, 50%, 75%, 100% radar radius ───
    struct Ring { GLuint vao; int count; };
    std::vector<Ring> rings;
    float radii[] = { 0.22f, 0.44f, 0.66f, RADAR_R };
    for (float r : radii) {
        auto pts = makeCircle(r, SEGMENTS);
        rings.push_back({ makeVAO(pts), (int)pts.size() });
    }

    // ── Background colour: near-black green-tinted ────────────────────
    glClearColor(0.02f, 0.06f, 0.02f, 1.0f);

    // ── Enable blending for alpha (needed for trail effect later) ─────
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

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

        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(prog);

        // Draw range rings: outer ring brighter, inner rings dimmer
        glm::vec3 greenDim  = { 0.0f, 0.5f, 0.15f };
        glm::vec3 greenBold = { 0.0f, 0.9f, 0.3f };

        for (int i = 0; i < (int)rings.size(); i++) {
            float alpha = (i == (int)rings.size()-1) ? 0.8f : 0.3f;
            glm::vec3 col = (i == (int)rings.size()-1) ? greenBold : greenDim;
            setColour(prog, col, alpha);
            glBindVertexArray(rings[i].vao);
            glDrawArrays(GL_LINE_STRIP, 0, rings[i].count);
        }

        glfwSwapBuffers(win);
    }

    glfwTerminate();
    return 0;
}

Build and Run

Build and confirm rings are visibleShell
cd C:\Labs\Capstone\RadarGL
cmake --build build --config Release
build\Release\RadarGL.exe
🏆 Stage 2 Milestone
Four Green Rings on Screen
You should see four concentric circles — bright outer boundary ring, dimmer inner range rings — on a dark green-tinted background. No animation yet. That's correct. The rendering pipeline is working.

Stage 2 Checkpoints

Shader files saved and no compile errors in terminal
If you see "VERT ERROR" or "FRAG ERROR" output, check for typos in the GLSL files
code
Four green rings visible on dark background
Outer ring brighter than inner rings. Circle is symmetric (looks round, not oval)
test
You can explain: why does makeCircle() use cos() and sin()?
If you can't answer this, re-read the NDC coordinate section above before moving on
think
Phase 1 · Stage 3 — OpenGL

The Rotating Sweep Line

Add real-time animation. The sweep line is a single line from (0,0) to the edge of the radar circle. Its angle changes every frame via a uniform. This is your first animated draw call.

🧠 Think first

The sweep line is TWO vertices: the center (0, 0) and the tip at angle θ. The tip's NDC position is (RADAR_R × sin(θ), RADAR_R × cos(θ)). Every frame, θ increases by a small delta. Instead of updating the VBO each frame (expensive), pass θ as a uniform and compute the tip position inside the vertex shader for the second vertex. The center vertex always stays at (0,0).

Update the Vertex Shader

Replace the content of shaders/radar.vert with this version that handles the sweep line rotation:

shaders/radar.vert — updated with sweep line rotationGLSL
#version 330 core

layout(location = 0) in vec2 inPos;

uniform float uAngle;    // current sweep angle in radians
uniform float uAlpha;
uniform  int  uIsSweep;  // 1 = this is a sweep line draw, 0 = normal
uniform float uRadarR;   // radar radius in NDC (passed from C++)

void main() {
    vec2 pos = inPos;

    if (uIsSweep == 1) {
        // Vertex 0 = center, stays at (0,0)
        // Vertex 1 = tip, computed from angle
        // We pass vertex index via inPos.x: 0.0 = center, 1.0 = tip
        if (inPos.x > 0.5) {  // tip vertex
            pos = vec2(uRadarR * sin(uAngle), uRadarR * cos(uAngle));
        } else {              // center vertex
            pos = vec2(0.0, 0.0);
        }
    }

    gl_Position = vec4(pos, 0.0, 1.0);
}

Add Sweep Line to main.cpp

Add these additions to your existing main.cpp. Find the section comment and insert the new code there:

main.cpp additions — add after makeVAO helper, before main()C++
// ── Sweep line: two vertices — 0.0 (center) and 1.0 (tip) ───────────
// The vertex shader uses this "index" to compute actual NDC positions.
// This VBO never changes — the angle comes in via uAngle uniform.
GLuint makeSweepLine() {
    float pts[] = {
        0.0f, 0.0f,   // vertex 0: center (inPos.x = 0.0 → stays at origin)
        1.0f, 0.0f    // vertex 1: tip    (inPos.x = 1.0 → rotated by shader)
    };
    GLuint vao, vbo;
    glGenVertexArrays(1, &vao); glBindVertexArray(vao);
    glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,
                          2*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
    return vao;
}
main.cpp — inside main(), add after ring setup, before render loopC++
// Sweep line VAO
GLuint sweepVAO = makeSweepLine();
float  sweepAngle = 0.0f;                // current angle in radians
const float SWEEP_SPEED = 1.2f;         // radians per second
float lastTime = (float)glfwGetTime();
main.cpp — inside the render loop, after drawing rings, before SwapBuffersC++
// ── Update sweep angle ────────────────────────────────────────────
float now   = (float)glfwGetTime();
float delta = now - lastTime; lastTime = now;
sweepAngle += SWEEP_SPEED * delta;
if (sweepAngle > 2.0f * PI) sweepAngle -= 2.0f * PI;

// ── Draw sweep line ───────────────────────────────────────────────
glUniform1f(glGetUniformLocation(prog, "uAngle"),  sweepAngle);
glUniform1f(glGetUniformLocation(prog, "uRadarR"), RADAR_R);
glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 1);
setColour(prog, { 0.0f, 1.0f, 0.4f }, 0.95f); // bright green
glLineWidth(2.0f);
glBindVertexArray(sweepVAO);
glDrawArrays(GL_LINES, 0, 2);
glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 0); // reset
glLineWidth(1.0f);
🏆 Stage 3 Milestone
The Radar Is Alive — Sweep Rotating
A bright green line rotates clockwise around the radar scope. This is your first real-time animation using a uniform. The sweep speed is controlled by SWEEP_SPEED — try changing it to 3.0f and rebuild to feel the difference.

Stage 3 Checkpoints

Bright sweep line rotates smoothly around the scope
One full rotation takes roughly 5 seconds at SWEEP_SPEED=1.2
test
Animation uses delta time — speed is frame-rate independent
Test: add glfwSwapInterval(0) before the loop. The sweep speed should not change even if FPS jumps to 300+
think
Phase 1 · Stage 4 — OpenGL

Blip Contacts

Add radar contacts — the small bright dots that represent tracked objects. Each blip has a polar position (bearing, range). You convert these to NDC and render them as GL_POINTS with a custom size.

🧠 Think first — how to store blips

Define a Blip struct with bearing (0–360°), range (0–1), and age (seconds since sweep last passed over it — controls brightness). Upload all blip NDC positions to one VBO. Redraw with GL_POINTS. The brightness fade is just the alpha uniform: age drives alpha. A newly-hit blip = alpha 1.0, old blip = alpha 0.1.

main.cpp — Blip struct + data + render (add to existing file)C++
// ── Blip contact definition ───────────────────────────────────────────
struct Blip {
    float bearing;  // degrees, 0=North, clockwise
    float range;    // 0.0 (center) to 1.0 (outer ring)
    float age;      // seconds since last sweep — drives fade
};

// Hardcoded contact list — in a real system this comes from sensor data
std::vector<Blip> contacts = {
    { 045.0f, 0.60f, 0.1f },   // NE, medium range
    { 135.0f, 0.35f, 1.8f },   // SE, short range, older
    { 260.0f, 0.75f, 0.4f },   // W, long range, fresh
    { 340.0f, 0.50f, 2.5f },   // NNW, medium range, fading
    { 090.0f, 0.85f, 0.8f },   // E, far range
};

// ── Convert polar blip to NDC vec2 ────────────────────────────────────
glm::vec2 blipNDC(const Blip& b) {
    float rad = b.bearing * PI / 180.0f;
    float r   = b.range * RADAR_R;
    return { r * sin(rad), r * cos(rad) };
}

// ── Build blip VBO from contact list ─────────────────────────────────
GLuint makeBlipVAO(const std::vector<Blip>& blips) {
    std::vector<glm::vec2> pts;
    for (auto& b : blips) pts.push_back(blipNDC(b));
    return makeVAO(pts); // reuse existing makeVAO helper
}

// ── Inside render loop: draw blips ───────────────────────────────────
// (add after sweep line draw, before SwapBuffers)
glPointSize(7.0f);                        // pixel size of GL_POINTS
glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 0); // not a sweep draw
for (int i = 0; i < (int)contacts.size(); i++) {
    float age   = contacts[i].age;
    float alpha = glm::clamp(1.0f - age / 3.0f, 0.1f, 1.0f);
    // age 0 → alpha 1.0 (just hit by sweep), age 3 → alpha 0.1 (faded)

    setColour(prog, { 0.0f, 1.0f, 0.45f }, alpha);
    glBindVertexArray(blipVAO);
    glDrawArrays(GL_POINTS, i, 1);   // draw one point at offset i

    // Tick the age forward each frame (simulates elapsed time)
    contacts[i].age += delta;
    if (contacts[i].age > 3.5f) contacts[i].age = 0.05f; // respawn
}
glPointSize(1.0f);
✅ Where to declare blipVAO

Declare GLuint blipVAO = makeBlipVAO(contacts); in main() after the sweep line setup, before the render loop. The VAO is built once — the per-frame fade is controlled only by the alpha uniform, not by re-uploading data.

Stage 4 Checkpoints

Five green dots visible at correct positions on the scope
Check: a blip at bearing 090° range 0.85 should appear near the right edge
test
Blips cycle from bright to dim and respawn
Each blip fades over ~3 seconds then reappears bright. This simulates the sweep refreshing a contact.
test
Phase 1 · Stage 5 — OpenGL

Phosphor Trail Effect

The glowing green trail behind the sweep line is what makes a radar display look authentic. It emerges from a clever use of alpha blending — no special effects needed.

🧠 The trick — it's simpler than you think

Do not clear the screen to full black every frame. Instead, draw a near-opaque dark rectangle over the entire screen first — covering about 97% of the previous frame. Old bright pixels get 97% darker. New pixels (the sweep line) are drawn at full brightness. After many frames: the sweep tip is bright, the recent trail is medium green, the old trail has faded. This is exactly how a real phosphor CRT works.

main.cpp — full-screen quad for trail fade (add after blip VAO setup)C++
// ── Full-screen fade quad: covers entire NDC space ────────────────────
// Every frame we draw this with alpha ~0.06, darkening everything slightly.
// Old bright pixels from previous frames fade slowly — creating the trail.
float quadVerts[] = {
    -1.0f, -1.0f,   1.0f, -1.0f,   1.0f,  1.0f,
    -1.0f, -1.0f,   1.0f,  1.0f,  -1.0f,  1.0f
};
GLuint fadeVAO, fadeVBO;
glGenVertexArrays(1, &fadeVAO); glBindVertexArray(fadeVAO);
glGenBuffers(1, &fadeVBO); glBindBuffer(GL_ARRAY_BUFFER, fadeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
main.cpp — render loop order (REPLACE your old glClear approach)C++
// ═══ RENDER ORDER — This order is critical for the trail effect ═══
// 1. Draw fade quad        — darkens everything from last frame
// 2. Draw range rings      — always at same brightness (re-drawn fresh)
// 3. Draw sweep line       — full brightness at current angle
// 4. Draw blips            — brightness based on age
// NOTE: NO glClear() — we never fully clear, the fade quad does it

// ── 1. Fade quad: draw a semi-transparent black over everything ───
glUniform1i(glGetUniformLocation(prog, "uIsSweep"), 0);
setColour(prog, { 0.01f, 0.04f, 0.01f }, 0.18f);
// Alpha 0.18 means: each frame retains 82% of previous brightness
// Lower alpha = longer, brighter trail. 0.18 is a good starting point.
glBindVertexArray(fadeVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);

// ── 2. Range rings (drawn fresh each frame over the faded bg) ────
/* ... your existing ring draw code here ... */

// ── 3. Sweep line ─────────────────────────────────────────────────
/* ... your existing sweep draw code here ... */

// ── 4. Blips ──────────────────────────────────────────────────────
/* ... your existing blip draw code here ... */
🏆 Stage 5 Milestone — The Big One
Full Phosphor Radar Display Running
The sweep line leaves a glowing green trail that fades as the line moves away. Blips persist after the sweep passes and fade slowly. This looks like real radar. You built this with 200 lines of C++ and 20 lines of GLSL — no engine, no framework, pure OpenGL.

Stage 5 Checkpoints

Green trail fades behind the rotating sweep line
The trail should look like a decaying glow, not a hard line. Adjust alpha (0.18f) to taste.
test
You can explain: why does removing glClear() create the trail?
The framebuffer retains previous frame's pixels. The fade quad darkens them, not erases them. The sweep line writes bright pixels on top.
think
Phase 1 · Stage 6 — OpenGL

HUD Overlay — GLFW Window Title

For this project, the HUD is added using GLFW's window title and on-screen geometry rather than a full text rendering system (which requires a font atlas and is a project in itself). We add angular tick marks and a center dot to complete the tactical look.

main.cpp — add heading ticks + center dot + window title updateC++
// ── Build heading tick marks (every 30°, short lines at edge) ────────
std::vector<glm::vec2> makeTickMarks() {
    std::vector<glm::vec2> pts;
    for (int deg = 0; deg < 360; deg += 30) {
        float a  = deg * PI / 180.0f;
        float r1 = RADAR_R * 0.93f;  // inner end of tick
        float r2 = RADAR_R;           // outer end = on the ring
        pts.push_back({ r1 * sin(a), r1 * cos(a) });
        pts.push_back({ r2 * sin(a), r2 * cos(a) });
    }
    return pts;
}

// ── Center dot: small circle to mark own-ship position ───────────────
auto tickPts    = makeTickMarks();
GLuint tickVAO  = makeVAO(tickPts);
auto centerPts  = makeCircle(0.015f, 16);   // tiny circle
GLuint centerVAO = makeVAO(centerPts);

// ── Inside render loop: draw ticks and center ─────────────────────────
setColour(prog, { 0.0f, 0.6f, 0.2f }, 0.55f);
glBindVertexArray(tickVAO);
glDrawArrays(GL_LINES, 0, (int)tickPts.size());

setColour(prog, { 0.0f, 1.0f, 0.5f }, 1.0f);
glBindVertexArray(centerVAO);
glDrawArrays(GL_LINE_LOOP, 0, (int)centerPts.size());

// ── Update window title as HUD (cheap but effective) ─────────────────
char titleBuf[128];
int contacts_count = (int)contacts.size();
float headingDisplay = fmod(sweepAngle * 180.0f / PI, 360.0f);
snprintf(titleBuf, sizeof(titleBuf),
         "RADAR DISPLAY  |  SWEEP: %03.0f°  |  CONTACTS: %d  |  RR Skillverse",
         headingDisplay, contacts_count);
glfwSetWindowTitle(win, titleBuf);
🏆 Phase 1 Complete!
OpenGL Radar Display — Fully Working
Concentric rings, rotating sweep with phosphor trail, fading blip contacts, heading ticks, center dot, live HUD in title bar. This is a complete, professional-looking application built on everything from Days 1–3. Take a screenshot. This is your OpenGL capstone.

Phase 1 Final Checkpoints

12 heading tick marks visible at 30° intervals on outer ring
Ticks at 000° (North=up), 030°, 060°, 090° (East=right)... and so on
test
All of Phase 1 running smoothly — take a screenshot
Rings + sweep trail + blips + ticks + center dot + live title. If anything is missing, go back to the relevant stage.
test
Phase 2 · Stage 1 — Vulkan

New Project — Vulkan Structure

You are rebuilding the same radar display in Vulkan. You already know what it should look like — so every Vulkan object you create has a clear visual purpose you can test immediately.

🧠 Why rebuild instead of port?

Porting OpenGL to Vulkan line-by-line teaches you nothing. Rebuilding with the same goal forces you to think: "what OpenGL did automatically here, what do I now explicitly control?" Every Vulkan object you create maps to something OpenGL was hiding from you. This is the entire point of Phase 2.

Create the Vulkan project folder alongside the GL oneShell
mkdir C:\Labs\Capstone\RadarVK
cd C:\Labs\Capstone\RadarVK
mkdir src shaders
C:\Labs\Capstone\RadarVK\
├── CMakeLists.txt
├── src\
│ └── main.cpp
└── shaders\
├── radar.vert ← GLSL 4.50 vertex shader
├── radar.frag ← GLSL 4.50 fragment shader
├── radar.vert.spv ← compiled by glslc (you generate this)
└── radar.frag.spv ← compiled by glslc
CMakeLists.txt for Vulkan projectCMake
cmake_minimum_required(VERSION 3.20)
project(RadarVK)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Vulkan REQUIRED)

set(GLFW_DIR $ENV{GLFW_DIR})
include_directories(${GLFW_DIR}/include)

set(GLM_DIR $ENV{GLM_DIR})
include_directories(${GLM_DIR})

add_executable(RadarVK src/main.cpp)

target_link_libraries(RadarVK
    Vulkan::Vulkan
    ${GLFW_DIR}/lib-vc2022/glfw3.lib
)

Compile the Shaders First

shaders/radar.vert — Vulkan GLSL (note Y negation)GLSL
#version 450

layout(location = 0) in  vec2 inPos;
layout(location = 0) out vec4 outColour;   // pass colour to frag

// Push constant: small per-draw data (angle, alpha, colour, flags)
// Max 128 bytes guaranteed — we use 32 bytes
layout(push_constant) uniform Push {
    float angle;     // sweep angle (radians)
    float alpha;     // opacity
    float radarR;    // radar radius in NDC
    int   isSweep;   // 1 = compute sweep tip from angle
    vec3  colour;    // RGB
    float _pad;      // alignment padding to 32 bytes
} push;

void main() {
    vec2 pos = inPos;
    if (push.isSweep == 1 && inPos.x > 0.5) {
        pos = vec2(push.radarR * sin(push.angle),
                   push.radarR * cos(push.angle));
    }
    // Vulkan: negate Y so North=up matches our OpenGL prototype
    gl_Position = vec4(pos.x, -pos.y, 0.0, 1.0);
    outColour   = vec4(push.colour, push.alpha);
}
shaders/radar.fragGLSL
#version 450
layout(location = 0) in  vec4 inColour;
layout(location = 0) out vec4 outColour;
void main() { outColour = inColour; }
Compile both shaders to SPIR-V — run every time you edit themShell
cd C:\Labs\Capstone\RadarVK
glslc shaders/radar.vert -o shaders/radar.vert.spv
glslc shaders/radar.frag -o shaders/radar.frag.spv
# Both commands must succeed with no output = success

Stage 1 Checkpoints

RadarVK folder created with src\ and shaders\ subfolders
build
Both .spv files created — glslc ran with no errors
Verify: dir shaders shows radar.vert.spv and radar.frag.spv
build
Phase 2 · Stage 2 — Vulkan

Vulkan Boilerplate — The Foundation Layer

Create the Vulkan object chain: Instance → Surface → PhysDevice → Device → Swapchain. Nothing visible yet — but when this runs without validation errors, the entire GPU pipeline is ready.

🧠 Map each object to its OpenGL equivalent

VkInstance = glewInit(). VkSurface = GLFW context. VkPhysicalDevice = the GPU you actually render on. VkDevice = the GL context that owns resources. VkSwapchain = glfwSwapBuffers() made explicit. This mapping is why you built the OpenGL version first — every Vulkan object now has a meaning.

src/main.cpp — complete Vulkan boilerplate (paste entire file)C++
// ═══════════════════════════════════════════════════════════════════
// RadarVK — Capstone Project · RR Skillverse
// Phase 2: Vulkan Radar Display
// Build: cmake -B build -G "Visual Studio 17 2022" -A x64
//        cmake --build build --config Release
//        (run from project root — shaders/ must be present)
// ═══════════════════════════════════════════════════════════════════
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <vulkan/vulkan.h>
#include <glm/glm.hpp>
#include <iostream>
#include <vector>
#include <fstream>
#include <cmath>
#include <stdexcept>
#include <cstring>

// ── Constants ─────────────────────────────────────────────────────────
const int   WIN_W = 900, WIN_H = 900;
const float PI     = 3.14159265358979f;
const float RADAR_R = 0.88f;
const int   SEGMENTS = 360;
const char* VALIDATION = "VK_LAYER_KHRONOS_validation";

// ── Push constant layout (must match shaders/radar.vert EXACTLY) ──────
struct PushData {
    float angle, alpha, radarR;
    int   isSweep;
    float r, g, b, _pad;  // 32 bytes total
};

// ── Helper: load SPIR-V file ───────────────────────────────────────────
std::vector<char> readSpv(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 sz = f.tellg(); std::vector<char> buf(sz);
    f.seekg(0); f.read(buf.data(), sz); return buf;
}

// ── Helper: create shader module from .spv bytes ──────────────────────
VkShaderModule makeShaderMod(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 m; vkCreateShaderModule(dev, &ci, nullptr, &m); return m;
}

// ── Generate circle vertices ───────────────────────────────────────────
std::vector<glm::vec2> makeCircle(float r, int segs) {
    std::vector<glm::vec2> pts;
    for (int i = 0; i <= segs; i++) {
        float a = (float)i / segs * 2.0f * PI;
        pts.push_back({ r * cos(a), r * sin(a) });
    }
    return pts;
}

// ═══════════════════════════════════════════════════════════════════════
// RadarApp class — all Vulkan state in one place
// ═══════════════════════════════════════════════════════════════════════
class RadarApp {
public:
    GLFWwindow*        window     = nullptr;
    VkInstance         instance   = VK_NULL_HANDLE;
    VkSurfaceKHR       surface    = VK_NULL_HANDLE;
    VkPhysicalDevice   physDev    = VK_NULL_HANDLE;
    VkDevice           device     = VK_NULL_HANDLE;
    VkQueue            gfxQueue   = VK_NULL_HANDLE;
    uint32_t           gfxFamily  = 0;
    VkSwapchainKHR     swapchain  = VK_NULL_HANDLE;
    VkFormat           swapFmt    = VK_FORMAT_UNDEFINED;
    VkExtent2D         swapExt    = {};
    std::vector<VkImage>      swapImgs;
    std::vector<VkImageView>  swapViews;
    std::vector<VkFramebuffer> framebuffers;
    VkRenderPass       renderPass = VK_NULL_HANDLE;
    VkPipelineLayout   pipeLayout = VK_NULL_HANDLE;
    VkPipeline         pipeline   = VK_NULL_HANDLE;
    VkCommandPool      cmdPool    = VK_NULL_HANDLE;
    VkCommandBuffer    cmdBuf     = VK_NULL_HANDLE;
    VkSemaphore        imgAvail   = VK_NULL_HANDLE;
    VkSemaphore        renderDone = VK_NULL_HANDLE;
    VkFence            inFlight   = VK_NULL_HANDLE;

    // Geometry buffers (one VkBuffer+VkDeviceMemory per ring + sweep + blips)
    struct GeomBuf { VkBuffer buf; VkDeviceMemory mem; int count; };
    std::vector<GeomBuf> rings;
    GeomBuf              sweepBuf, blipBuf, fadeBuf;

    float sweepAngle = 0.0f;

    // ── Init ──────────────────────────────────────────────────────────
    void init() {
        initWindow();
        createInstance();
        createSurface();
        pickPhysDevice();
        createDevice();
        createSwapchain();
        createImageViews();
        createRenderPass();
        createPipeline();
        createFramebuffers();
        createCommandPool();
        createGeometry();
        createSyncObjects();
        allocCommandBuffer();
    }

    // ── Run ───────────────────────────────────────────────────────────
    void run() {
        float last = (float)glfwGetTime();
        while (!glfwWindowShouldClose(window)) {
            glfwPollEvents();
            if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
                glfwSetWindowShouldClose(window, true);
            float now = (float)glfwGetTime();
            sweepAngle += 1.2f * (now - last); last = now;
            if (sweepAngle > 2.0f * PI) sweepAngle -= 2.0f * PI;
            drawFrame();
        }
        vkDeviceWaitIdle(device);
    }

    // ── Cleanup ───────────────────────────────────────────────────────
    void cleanup() {
        vkDestroySemaphore(device, renderDone, nullptr);
        vkDestroySemaphore(device, imgAvail,   nullptr);
        vkDestroyFence(device, inFlight,       nullptr);
        vkDestroyCommandPool(device, cmdPool,  nullptr);
        for (auto& fb : framebuffers) vkDestroyFramebuffer(device, fb, nullptr);
        vkDestroyPipeline(device, pipeline,    nullptr);
        vkDestroyPipelineLayout(device, pipeLayout, nullptr);
        vkDestroyRenderPass(device, renderPass, nullptr);
        for (auto& v : swapViews) vkDestroyImageView(device, v, nullptr);
        vkDestroySwapchainKHR(device, swapchain, nullptr);
        vkDestroyDevice(device,   nullptr);
        vkDestroySurfaceKHR(instance, surface, nullptr);
        vkDestroyInstance(instance,  nullptr);
        glfwDestroyWindow(window); glfwTerminate();
    }

private:
    void initWindow() {
        glfwInit();
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
        glfwWindowHint(GLFW_RESIZABLE,  GLFW_FALSE);
        window = glfwCreateWindow(WIN_W, WIN_H,
            "Tactical Radar — Vulkan — RR Skillverse",
            nullptr, nullptr);
    }

    void createInstance() {
        uint32_t ec = 0; auto glfwExts = glfwGetRequiredInstanceExtensions(&ec);
        VkApplicationInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
        ai.apiVersion = VK_API_VERSION_1_3;
        VkInstanceCreateInfo ci{};
        ci.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
        ci.pApplicationInfo        = &ai;
        ci.enabledExtensionCount   = ec;
        ci.ppEnabledExtensionNames = glfwExts;
        ci.enabledLayerCount       = 1;
        ci.ppEnabledLayerNames     = &VALIDATION;
        if (vkCreateInstance(&ci, nullptr, &instance) != VK_SUCCESS)
            throw std::runtime_error("vkCreateInstance failed");
        std::cout << "[1] Instance OK\n";
    }

    void createSurface() {
        glfwCreateWindowSurface(instance, window, nullptr, &surface);
        std::cout << "[2] Surface OK\n";
    }

    void pickPhysDevice() {
        uint32_t cnt = 0; vkEnumeratePhysicalDevices(instance, &cnt, nullptr);
        std::vector<VkPhysicalDevice> devs(cnt);
        vkEnumeratePhysicalDevices(instance, &cnt, devs.data());
        for (auto& d : devs) {
            VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(d, &p);
            if (p.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { physDev = d; break; }
            if (!physDev) physDev = d;
        }
        VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(physDev, &p);
        std::cout << "[3] GPU: " << p.deviceName << "\n";
    }

    void createDevice() {
        // Find graphics+present queue family
        uint32_t qc; vkGetPhysicalDeviceQueueFamilyProperties(physDev, &qc, nullptr);
        std::vector<VkQueueFamilyProperties> qfs(qc);
        vkGetPhysicalDeviceQueueFamilyProperties(physDev, &qc, qfs.data());
        for (uint32_t i = 0; i < qc; i++) {
            VkBool32 pres; vkGetPhysicalDeviceSurfaceSupportKHR(physDev, i, surface, &pres);
            if ((qfs[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && pres) { gfxFamily = i; break; }
        }
        float pri = 1.0f;
        VkDeviceQueueCreateInfo qci{};
        qci.sType            = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
        qci.queueFamilyIndex = gfxFamily; qci.queueCount = 1; qci.pQueuePriorities = &pri;
        const char* ext = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
        VkPhysicalDeviceFeatures feat{};
        VkDeviceCreateInfo dci{};
        dci.sType                   = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
        dci.queueCreateInfoCount    = 1; dci.pQueueCreateInfos = &qci;
        dci.enabledExtensionCount   = 1; dci.ppEnabledExtensionNames = &ext;
        dci.pEnabledFeatures        = &feat;
        if (vkCreateDevice(physDev, &dci, nullptr, &device) != VK_SUCCESS)
            throw std::runtime_error("vkCreateDevice failed");
        vkGetDeviceQueue(device, gfxFamily, 0, &gfxQueue);
        std::cout << "[4] Device + Queue OK\n";
    }

    void createSwapchain() {
        VkSurfaceCapabilitiesKHR caps;
        vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physDev, surface, &caps);
        swapExt = caps.currentExtent;

        uint32_t fmtCount; vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &fmtCount, nullptr);
        std::vector<VkSurfaceFormatKHR> fmts(fmtCount);
        vkGetPhysicalDeviceSurfaceFormatsKHR(physDev, surface, &fmtCount, fmts.data());
        swapFmt = fmts[0].format;
        for (auto& f : fmts)
            if (f.format == VK_FORMAT_B8G8R8A8_SRGB && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
                swapFmt = f.format;

        VkSwapchainCreateInfoKHR sci{};
        sci.sType            = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
        sci.surface          = surface;
        sci.minImageCount    = caps.minImageCount + 1;
        sci.imageFormat      = swapFmt;
        sci.imageColorSpace  = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
        sci.imageExtent      = swapExt;
        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      = VK_PRESENT_MODE_FIFO_KHR;
        sci.clipped          = VK_TRUE;
        if (vkCreateSwapchainKHR(device, &sci, nullptr, &swapchain) != VK_SUCCESS)
            throw std::runtime_error("vkCreateSwapchainKHR failed");
        uint32_t ic; vkGetSwapchainImagesKHR(device, swapchain, &ic, nullptr);
        swapImgs.resize(ic);
        vkGetSwapchainImagesKHR(device, swapchain, &ic, swapImgs.data());
        std::cout << "[5] Swapchain OK (" << ic << " images)\n";
    }

    void createImageViews() {
        swapViews.resize(swapImgs.size());
        for (size_t i = 0; i < swapImgs.size(); i++) {
            VkImageViewCreateInfo vi{};
            vi.sType    = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
            vi.image    = swapImgs[i];
            vi.viewType = VK_IMAGE_VIEW_TYPE_2D;
            vi.format   = swapFmt;
            vi.components = { VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
                               VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY };
            vi.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
            vkCreateImageView(device, &vi, nullptr, &swapViews[i]);
        }
    }

    void createRenderPass() {
        VkAttachmentDescription att{};
        att.format         = swapFmt;
        att.samples        = VK_SAMPLE_COUNT_1_BIT;
        att.loadOp         = VK_ATTACHMENT_LOAD_OP_LOAD;
        // LOAD (not CLEAR): we keep the previous frame for the phosphor trail effect.
        // The fade pass draws a semi-transparent quad to darken the old frame.
        // This is the exact Vulkan equivalent of what we did in OpenGL.
        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    = ⊂
        rpi.dependencyCount = 1; rpi.pDependencies = &dep;
        vkCreateRenderPass(device, &rpi, nullptr, &renderPass);
        std::cout << "[6] RenderPass OK\n";
    }

    void createPipeline() {
        auto vs = makeShaderMod(device, readSpv("shaders/radar.vert.spv"));
        auto fs = makeShaderMod(device, readSpv("shaders/radar.frag.spv"));

        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 = vs; 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 = fs; stages[1].pName = "main";

        VkVertexInputBindingDescription bind{0, sizeof(glm::vec2), VK_VERTEX_INPUT_RATE_VERTEX};
        VkVertexInputAttributeDescription attr{0, 0, VK_FORMAT_R32G32_SFLOAT, 0};
        VkPipelineVertexInputStateCreateInfo vi{};
        vi.sType                           = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
        vi.vertexBindingDescriptionCount   = 1; vi.pVertexBindingDescriptions   = &bind;
        vi.vertexAttributeDescriptionCount = 1; vi.pVertexAttributeDescriptions = &attr;

        VkPipelineInputAssemblyStateCreateInfo ia{};
        ia.sType    = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
        ia.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
        // LINE_STRIP: works for rings (line loop) and lines.
        // We'll switch topology per draw using dynamic state.

        VkDynamicState dynStates[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR,
                                          VK_DYNAMIC_STATE_LINE_WIDTH, VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY };
        VkPipelineDynamicStateCreateInfo dynCI{};
        dynCI.sType             = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
        dynCI.dynamicStateCount = 4; dynCI.pDynamicStates = dynStates;

        VkPipelineViewportStateCreateInfo vpS{};
        vpS.sType         = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
        vpS.viewportCount = 1; vpS.scissorCount = 1;

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

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

        VkPipelineColorBlendAttachmentState blAtt{};
        blAtt.colorWriteMask = VK_COLOR_COMPONENT_R_BIT|VK_COLOR_COMPONENT_G_BIT|
                                VK_COLOR_COMPONENT_B_BIT|VK_COLOR_COMPONENT_A_BIT;
        blAtt.blendEnable    = VK_TRUE;
        blAtt.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
        blAtt.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
        blAtt.colorBlendOp        = VK_BLEND_OP_ADD;
        blAtt.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
        blAtt.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
        blAtt.alphaBlendOp        = VK_BLEND_OP_ADD;

        VkPipelineColorBlendStateCreateInfo bl{};
        bl.sType           = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
        bl.attachmentCount = 1; bl.pAttachments = &blAtt;

        VkPushConstantRange pcRange{};
        pcRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
        pcRange.offset     = 0;
        pcRange.size       = sizeof(PushData); // 32 bytes

        VkPipelineLayoutCreateInfo plCI{};
        plCI.sType                  = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
        plCI.pushConstantRangeCount = 1;
        plCI.pPushConstantRanges    = &pcRange;
        vkCreatePipelineLayout(device, &plCI, nullptr, &pipeLayout);

        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      = &vpS;
        gpCI.pRasterizationState = &rast;
        gpCI.pMultisampleState   = &ms;
        gpCI.pColorBlendState    = &bl;
        gpCI.pDynamicState       = &dynCI;
        gpCI.layout              = pipeLayout;
        gpCI.renderPass          = renderPass;
        gpCI.subpass             = 0;
        vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &gpCI, nullptr, &pipeline);
        vkDestroyShaderModule(device, vs, nullptr);
        vkDestroyShaderModule(device, fs, nullptr);
        std::cout << "[7] Pipeline OK\n";
    }

    void createFramebuffers() {
        framebuffers.resize(swapViews.size());
        for (size_t i = 0; i < swapViews.size(); i++) {
            VkFramebufferCreateInfo fi{};
            fi.sType           = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
            fi.renderPass      = renderPass;
            fi.attachmentCount = 1; fi.pAttachments = &swapViews[i];
            fi.width  = swapExt.width; fi.height = swapExt.height; fi.layers = 1;
            vkCreateFramebuffer(device, &fi, nullptr, &framebuffers[i]);
        }
    }

    void createCommandPool() {
        VkCommandPoolCreateInfo ci{};
        ci.sType            = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
        ci.queueFamilyIndex = gfxFamily;
        ci.flags            = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
        vkCreateCommandPool(device, &ci, nullptr, &cmdPool);
    }

    // ── Allocate a VkBuffer + bind VkDeviceMemory ─────────────────────
    GeomBuf uploadGeom(const std::vector<glm::vec2>& pts, VkBufferUsageFlags usage) {
        VkDeviceSize sz = pts.size() * sizeof(glm::vec2);
        VkBufferCreateInfo bci{};
        bci.sType       = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
        bci.size        = sz; bci.usage = usage;
        bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
        GeomBuf gb; gb.count = (int)pts.size();
        vkCreateBuffer(device, &bci, nullptr, &gb.buf);

        VkMemoryRequirements req; vkGetBufferMemoryRequirements(device, gb.buf, &req);
        VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(physDev, &mp);
        uint32_t mIdx = UINT32_MAX;
        auto need = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
        for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
            if ((req.memoryTypeBits &(1u<<i)) && ((mp.memoryTypes[i].propertyFlags&need)==need)) { mIdx=i; break; }

        VkMemoryAllocateInfo mai{};
        mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
        mai.allocationSize = req.size; mai.memoryTypeIndex = mIdx;
        vkAllocateMemory(device, &mai, nullptr, &gb.mem);
        vkBindBufferMemory(device, gb.buf, gb.mem, 0);

        void* data; vkMapMemory(device, gb.mem, 0, sz, 0, &data);
        memcpy(data, pts.data(), sz);
        vkUnmapMemory(device, gb.mem);
        return gb;
    }

    void createGeometry() {
        float radii[] = { 0.22f, 0.44f, 0.66f, RADAR_R };
        for (float r : radii)
            rings.push_back(uploadGeom(makeCircle(r, SEGMENTS), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT));

        float swPts[] = { 0.0f, 0.0f, 1.0f, 0.0f };
        std::vector<glm::vec2> swVec = {{0.0f,0.0f},{1.0f,0.0f}};
        sweepBuf = uploadGeom(swVec, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);

        std::vector<glm::vec2> blips = {
            { RADAR_R * 0.6f * sin(45.0f*PI/180),  RADAR_R * 0.6f * cos(45.0f*PI/180) },
            { RADAR_R * 0.35f* sin(135.0f*PI/180), RADAR_R * 0.35f* cos(135.0f*PI/180) },
            { RADAR_R * 0.75f* sin(260.0f*PI/180), RADAR_R * 0.75f* cos(260.0f*PI/180) },
        };
        blipBuf = uploadGeom(blips, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);

        std::vector<glm::vec2> quad = {
            {-1,-1},{1,-1},{1,1},{-1,-1},{1,1},{-1,1}
        };
        fadeBuf = uploadGeom(quad, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);

        std::cout << "[8] Geometry uploaded\n";
    }

    void allocCommandBuffer() {
        VkCommandBufferAllocateInfo ai{};
        ai.sType              = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        ai.commandPool        = cmdPool;
        ai.level              = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        ai.commandBufferCount = 1;
        vkAllocateCommandBuffers(device, &ai, &cmdBuf);
    }

    void createSyncObjects() {
        VkSemaphoreCreateInfo si{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO };
        VkFenceCreateInfo     fi{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
        fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; // start signaled so first frame doesn't hang
        vkCreateSemaphore(device, &si, nullptr, &imgAvail);
        vkCreateSemaphore(device, &si, nullptr, &renderDone);
        vkCreateFence(device,     &fi, nullptr, &inFlight);
        std::cout << "[9] Sync objects OK\n";
    }

    // ── Push helper ───────────────────────────────────────────────────
    void push(float angle, float alpha, float r, float g, float b,
              int isSweep = 0) {
        PushData pd{ angle, alpha, RADAR_R, isSweep, r, g, b, 0.0f };
        vkCmdPushConstants(cmdBuf, pipeLayout,
            VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
            0, sizeof(PushData), &pd);
    }

    void drawFrame() {
        // 1. Wait for previous frame to finish
        vkWaitForFences(device, 1, &inFlight, VK_TRUE, UINT64_MAX);
        vkResetFences(device, 1, &inFlight);

        // 2. Acquire next swapchain image
        uint32_t imgIdx;
        vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, imgAvail, VK_NULL_HANDLE, &imgIdx);

        // 3. Record commands
        vkResetCommandBuffer(cmdBuf, 0);
        VkCommandBufferBeginInfo bi{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
        vkBeginCommandBuffer(cmdBuf, &bi);

        VkClearValue clear{}; clear.color = {{ 0.0f, 0.0f, 0.0f, 1.0f }};
        VkRenderPassBeginInfo rpBI{};
        rpBI.sType           = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        rpBI.renderPass      = renderPass;
        rpBI.framebuffer     = framebuffers[imgIdx];
        rpBI.renderArea      = {{0,0}, swapExt};
        rpBI.clearValueCount = 1; rpBI.pClearValues = &clear;
        vkCmdBeginRenderPass(cmdBuf, &rpBI, VK_SUBPASS_CONTENTS_INLINE);

        vkCmdBindPipeline(cmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);

        VkViewport vp{ 0, 0, (float)swapExt.width, (float)swapExt.height, 0, 1 };
        VkRect2D   sc{ {0,0}, swapExt };
        vkCmdSetViewport(cmdBuf, 0, 1, &vp);
        vkCmdSetScissor(cmdBuf,  0, 1, &sc);
        vkCmdSetLineWidth(cmdBuf, 1.0f);

        // ═══ Draw order (mirrors the OpenGL version exactly) ═══════════

        // DRAW 1 — Fade quad: darkens previous frame for phosphor trail
        vkCmdSetPrimitiveTopology(cmdBuf, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);
        VkDeviceSize off = 0;
        vkCmdBindVertexBuffers(cmdBuf, 0, 1, &fadeBuf.buf, &off);
        push(0, 0.18f, 0.01f, 0.04f, 0.01f); // near-black, low alpha
        vkCmdDraw(cmdBuf, 6, 1, 0, 0);

        // DRAW 2 — Range rings
        vkCmdSetPrimitiveTopology(cmdBuf, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP);
        for (int i = 0; i < (int)rings.size(); i++) {
            float a = (i == (int)rings.size()-1) ? 0.75f : 0.25f;
            push(0, a, 0.0f, i==(int)rings.size()-1 ? 0.9f : 0.45f, 0.15f);
            vkCmdBindVertexBuffers(cmdBuf, 0, 1, &rings[i].buf, &off);
            vkCmdDraw(cmdBuf, rings[i].count, 1, 0, 0);
        }

        // DRAW 3 — Sweep line
        vkCmdSetLineWidth(cmdBuf, 2.0f);
        push(sweepAngle, 0.95f, 0.0f, 1.0f, 0.4f, 1); // isSweep=1
        vkCmdBindVertexBuffers(cmdBuf, 0, 1, &sweepBuf.buf, &off);
        vkCmdDraw(cmdBuf, 2, 1, 0, 0);
        vkCmdSetLineWidth(cmdBuf, 1.0f);

        // DRAW 4 — Blip contacts as points
        vkCmdSetPrimitiveTopology(cmdBuf, VK_PRIMITIVE_TOPOLOGY_POINT_LIST);
        push(0, 0.9f, 0.0f, 1.0f, 0.5f);
        vkCmdBindVertexBuffers(cmdBuf, 0, 1, &blipBuf.buf, &off);
        vkCmdDraw(cmdBuf, blipBuf.count, 1, 0, 0);

        vkCmdEndRenderPass(cmdBuf);
        vkEndCommandBuffer(cmdBuf);

        // 4. Submit to queue
        VkPipelineStageFlags ws = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        VkSubmitInfo si{};
        si.sType                = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        si.waitSemaphoreCount   = 1; si.pWaitSemaphores   = &imgAvail;
        si.pWaitDstStageMask    = &ws;
        si.commandBufferCount   = 1; si.pCommandBuffers   = &cmdBuf;
        si.signalSemaphoreCount = 1; si.pSignalSemaphores = &renderDone;
        vkQueueSubmit(gfxQueue, 1, &si, inFlight);

        // 5. Present
        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(gfxQueue, &pi);
    }
};

int main() {
    std::cout << "\n=== Radar VK — Capstone Project · RR Skillverse ===\n\n";
    RadarApp app;
    try {
        app.init();
        std::cout << "\n[✓] All Vulkan objects created. Rendering...\n";
        std::cout << "    Press ESC to exit.\n\n";
        app.run();
        app.cleanup();
    } catch (const std::exception& e) {
        std::cerr << "ERROR: " << e.what() << "\n";
        return 1;
    }
    std::cout << "Done.\n";
    return 0;
}

Phase 2 Build + Run

Build and run the Vulkan radarShell
cd C:\Labs\Capstone\RadarVK

# Compile shaders (always first)
glslc shaders/radar.vert -o shaders/radar.vert.spv
glslc shaders/radar.frag -o shaders/radar.frag.spv

# Build
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release

# Run from project root
build\Release\RadarVK.exe
✅ Expected Terminal Output Before Window Opens

[1] Instance OK[2] Surface OK[3] GPU: NVIDIA GeForce RTX...[4] Device + Queue OK[5] Swapchain OK (3 images)[6] RenderPass OK[7] Pipeline OK[8] Geometry uploaded[9] Sync objects OK[✓] All Vulkan objects created. Rendering.... If any line is missing, check the validation layer output — it will name the exact failed call.

🏆 Phase 2 Complete — The Grand Finale
Vulkan Radar Display Running
The same radar display as Phase 1 — same rings, same sweep, same trail, same blips — but now running on explicit Vulkan. You created the instance, chose the GPU, declared every memory allocation, recorded the command buffer, managed semaphores and fences, and submitted the frame. Nothing was hidden. Nothing was guessed. Every pixel on screen was explicitly authorised by your code.

Phase 2 Checkpoints

Terminal shows [1] through [9] and [✓] — all objects created without errors
If you see an error, check validation layer output. The VUID tells you exactly which call failed.
build
Radar display visible — rings, sweep, trail, blips
Same visual as the OpenGL version, but running through the complete Vulkan pipeline you wrote
test
You can name the Vulkan equivalent of every OpenGL call you replaced
glGenBuffers → vkCreateBuffer+vkAllocateMemory. glUseProgram → vkCmdBindPipeline. glDrawArrays → vkCmdDraw after vkQueueSubmit. glfwSwapBuffers → vkAcquireNextImageKHR+vkQueuePresentKHR.
think
Final Stage · Polish

Final Polish & Known Extensions

Your radar display is working. Here are optional improvements to explore after completing the core project — each one is a natural next step using concepts from the handbook.

Optional Extensions (pick any one)

A
Dynamic blip data (OpenGL)
Pass a glUniform2fv array of blip positions updated each frame. Contacts that appear as the sweep passes over them — refresh the positions randomly when swept.
B
Coloured threat levels (both)
Add a uThreatLevel uniform (0=green, 1=amber, 2=red). Draw hostile contacts in red using a second colour in the fragment shader.
C
Zoom control (both)
Add keyboard input: press + to multiply RADAR_R by 0.9f (zoom in — contacts expand outward). This simulates changing the range scale.
D
VMA memory allocator (Vulkan)
Replace the manual vkAllocateMemory calls with the Vulkan Memory Allocator library (VMA). This is how production engines manage GPU memory. One include, three functions.
📌 Stopping Point Rule

If you do not have time for extensions, that is correct. The core project is complete. Extensions exist so students who finish early have meaningful work to do — not as requirements. A working, clean core project is worth more than a buggy feature-heavy one.

Final Checkpoints

Both RadarGL and RadarVK exit cleanly when ESC is pressed
No validation errors on shutdown. All resources destroyed. No "objects not destroyed" warnings.
test
Screenshot saved of both versions side by side
Keep these for your portfolio. You built hardware-accelerated real-time graphics from scratch.
think
Reflection

What You Just Built — and What It Means

A moment to understand what this week actually achieved.

One Week Ago vs Right Now

One week ago you knew
C++ basicsWhat a GPU is
Right now you can
Write GLSL shadersUse VAO/VBO/uniforms Build an OpenGL render loopControl alpha blending Create every Vulkan object from scratch Choose GPU memory types explicitly Record + submit command buffers Manage GPU-CPU synchronisation Use push constants Decompose a visual into geometry Debug with validation layers Build with CMake + glslc
🧠 The deeper lesson from this capstone

You did not just learn two APIs. You learned how to think about rendering problems: decompose visual into geometry, understand the pipeline, control exactly what the GPU does. OpenGL showed you the results. Vulkan showed you the mechanism. The radar display was the shared goal that made both lessons concrete. That is the pedagogy of "build the same thing twice" — you can compare them, and the comparison is the learning.

🏆 Capstone Complete
You Built a Tactical Radar Display from Scratch
In both OpenGL and Vulkan. With shaders you wrote, geometry you generated, memory you allocated, and a synchronisation model you explicitly managed. No engine. No framework. Just you, the GPU, and the hardware pipeline. That is what this week was about.

RR Skillverse — Capstone Project: Tactical Radar Display
By Raushan Ranjan · MCT | RR Skillverse, Noida
"Sweat in the right direction brings Peace, Money, and Respect."