Day 5 Capstone · 5 Labs · Full Tactical System
Maritime Tactical System
You are delivering the final integrated system for the USS Monterey's combat information centre. Five labs build it layer by layer: animated Vulkan scene via UBO, GPU compute sonar processing, multi-resource descriptor sets, a C# bridge for command console integration, and a 10-minute live capstone demo. This is the culmination of all 15 modules.
Descriptor Sets Vulkan Compute UBO / SSBO C# P/Invoke vkCmdDispatch Pipeline barriers
📁 Project Setup

Labs 1–3 use the NavalBridgeRenderer project from Day 4 as a base. Each lab extends or modifies a specific part. Labs 4–5 introduce a new C# console project and the capstone presentation.

Lab 1 of 5 ⬛ Advanced · ~45 min
Animate the Naval Bridge Renderer via UBO
Extend the Day 4 capstone (Lab 5) with a full descriptor set and UBO. Replace the hardcoded vertex positions with a vertex buffer, and drive the triangle rotation via an MVP matrix sent through a UBO at binding 0. One UBO per swapchain image. The triangle rotates continuously at 45°/sec.
📋 Concepts: VkDescriptorSetLayout · VkDescriptorPool · VkDescriptorSet · UBO per image ⚡ Do Demo 19 first
➕ What You Add Over Day 4 Lab 5
  • UBO struct: glm::mat4 model + view + proj
  • VkDescriptorSetLayout: binding 0 = uniform buffer, vertex stage
  • VkDescriptorPool + one VkDescriptorSet per swapchain image
  • One VkBuffer (HOST_VISIBLE | HOST_COHERENT) per swapchain image, persistently mapped
  • Update UBO each frame: model = rotate(t * 45deg, Z), view = lookAt, proj = perspective
  • vkCmdBindDescriptorSets before vkCmdDraw
  • Vertex shader reads UBO at layout(binding=0)
🎯 What You Will See
Window: Orange triangle rotating at 45°/sec driven by MVP matrix in UBO
Terminal: Frame count printed every 60 frames confirming loop is running
Validation: 0 errors — descriptor sets and UBO lifecycle correct
Objectives
  • UBO struct declared matching shader layout(std140) exactly
  • VkDescriptorSetLayout with binding 0 = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
  • VkDescriptorPool with descriptorCount = swapchain image count
  • One UBO buffer per swapchain image, persistently mapped
  • vkUpdateDescriptorSets called once to bind each buffer to its set
  • UBO updated per frame via memcpy to the mapped pointer for the acquired image index
  • vkCmdBindDescriptorSets called before vkCmdDraw in the render loop
  • ubo.proj[1][1] *= -1 applied to correct Vulkan Y-axis flip
✍ Common Mistakes

Forgetting proj[1][1] *= -1: Vulkan NDC has Y pointing down (OpenGL: Y up). Without this flip, the triangle is upside down. Wrong descriptor count in pool: Pool must have descriptorCount equal to the number of swapchain images — if you allocated 3 images but poolSize.descriptorCount = 1, vkAllocateDescriptorSets fails. Binding mismatch: The layout binding number must match the GLSL layout(binding = N) annotation exactly.

💡 Pipeline Layout Must Include the Descriptor Set Layout

When creating VkPipelineLayoutCreateInfo, set setLayoutCount = 1 and pSetLayouts = &dsLayout. If the pipeline layout does not include the descriptor set layout, vkCmdBindDescriptorSets will silently fail or produce validation errors — the shader will read undefined data.

✓ Lab 1 Deliverable
Animated Vulkan Scene via UBO
Orange triangle rotating at 45°/sec. All transformation data flows through the descriptor set and UBO. No per-frame vkQueueWaitIdle — proper semaphore/fence sync from Day 4 Lab 5 retained.
  • Triangle rotates smoothly without jitter or stutter
  • Validation layers: 0 errors reported
  • Can explain: why one UBO per swapchain image, not one shared UBO
Lab 2 of 5 ⬛ Advanced · ~50 min
GPU Compute Sonar Signal Processor
Implement a standalone Vulkan compute pass that processes a synthetic sonar return array on the GPU. 512 sonar samples (floats) are written to a host-visible storage buffer, a compute shader applies a simple signal processing kernel (noise floor subtraction + normalisation), and results are read back. Demonstrates how naval sonar returns can be GPU-processed at >100 million samples/second with zero CPU involvement during the compute phase.
📋 Concepts: VkComputePipeline · SSBO · vkCmdDispatch · pipeline barriers ⚡ Do Demo 20 first
➕ What You Build
  • Input SSBO: 512 floats simulating sonar amplitude returns (synthetic noise + signal)
  • Output SSBO: 512 floats, processed (noise floor = 0.1 subtracted, normalised 0–1)
  • Compute shader: one thread per sample, local_size_x = 64
  • Dispatch: ceil(512/64) = 8 workgroups
  • Pipeline barrier after dispatch (COMPUTE_SHADER → HOST)
  • CPU reads back results and prints first 8 processed values
  • Timing: measure total dispatch + wait time
🎯 What You Will See (Terminal)
Input samples [0–7]: Raw synthetic values (e.g. 0.05, 0.82, 0.13...)
Output samples [0–7]: Noise-subtracted and normalised (0.00, 0.72, 0.03...)
Timing: Processing time in microseconds + throughput in samples/sec
No window — headless compute, no swapchain
Compute shader (sonar_process.comp)
sonar_process.comp
GLSL
#version 450
layout(local_size_x = 64) in;

// binding 0: raw sonar return amplitudes (read-only)
layout(std430, binding = 0) readonly buffer InputSamples {
    float samples_in[];
};

// binding 1: processed output (write-only)
layout(std430, binding = 1) writeonly buffer OutputSamples {
    float samples_out[];
};

layout(push_constant) uniform PC {
    uint count;       // total sample count
    float noiseFloor; // noise threshold to subtract (0.1)
    float maxVal;     // max expected amplitude for normalisation (1.0)
} pc;

void main() {
    uint idx = gl_GlobalInvocationID.x;
    if (idx >= pc.count) return;

    float raw = samples_in[idx];

    // Noise floor subtraction: anything below noiseFloor is treated as noise
    float clean = max(raw - pc.noiseFloor, 0.0);

    // Normalise to 0..1 range relative to max expected amplitude
    float normalised = clean / (pc.maxVal - pc.noiseFloor);
    normalised = clamp(normalised, 0.0, 1.0);

    samples_out[idx] = normalised;
}
Objectives
  • 512-float input SSBO filled with synthetic sonar data (signal at index 100–110)
  • VkComputePipeline created from sonar_process.comp SPIR-V
  • Two SSBOs bound at binding 0 (input) and binding 1 (output)
  • Push constants: sample count, noise floor (0.1), max amplitude (1.0)
  • vkCmdDispatch(8, 1, 1) launched and timed
  • Pipeline barrier before CPU read-back
  • First 8 input and output values printed — output shows noise suppression
✓ Lab 2 Deliverable
GPU Sonar Signal Processor
512 sonar samples processed on GPU with noise subtraction and normalisation. Terminal output shows before/after values and throughput.
  • Noise floor correctly suppressed in output (samples below 0.1 → 0.0)
  • Signal peak at index 100–110 preserved and normalised to ~1.0
  • Pipeline barrier prevents CPU reading before GPU finishes
  • Throughput printed: >50 million samples/sec expected on discrete GPU
Lab 3 of 5 ⬛ Advanced · ~45 min · Builds on Lab 1
Multi-Resource Descriptor Sets — UBO + Texture Sampler
Extend Lab 1 to bind two resources in a single descriptor set: the UBO (binding 0, uniform buffer) and a texture sampler (binding 1, combined image sampler). Load a 64×64 solid-colour PNG (or generate a procedural texture in code), sample it in the fragment shader, and multiply the result by the vertex colour. This pattern is how all real Vulkan renderers pass both transformation data and textures to shaders.
📋 Concepts: VkImage · VkImageView · VkSampler · layout transition barriers · combined image sampler ⚡ Builds on Lab 1
➕ What You Add Over Lab 1
  • Procedural 64×64 RGBA texture generated in CPU memory (gradient)
  • VkImage + VkDeviceMemory (DEVICE_LOCAL) + staging buffer upload
  • Image layout transition: UNDEFINED → TRANSFER_DST → SHADER_READ_ONLY
  • VkImageView wrapping the VkImage
  • VkSampler (LINEAR filter, REPEAT wrap)
  • Descriptor layout: binding 0 = UBO, binding 1 = COMBINED_IMAGE_SAMPLER
  • Fragment shader samples the texture and multiplies by vertex colour
🎯 What You Will See
Window: Rotating triangle tinted by the procedural gradient texture — colour shifts as it rotates
Terminal: Texture upload size and image layout transitions confirmed
Image Layout Transition Chain (Required for Texture Upload)
  CPU creates staging buffer (HOST_VISIBLE)  →  memcpy texture data into it

  VkImage created (DEVICE_LOCAL, TRANSFER_DST_OPTIMAL initially undefined)

  Transition 1: image layout UNDEFINED → TRANSFER_DST_OPTIMAL
    pipeline barrier: src=TOP_OF_PIPE, dst=TRANSFER
    allows: vkCmdCopyBufferToImage()

  Transition 2: image layout TRANSFER_DST_OPTIMAL → SHADER_READ_ONLY_OPTIMAL
    pipeline barrier: src=TRANSFER, dst=FRAGMENT_SHADER
    allows: texture2D() in fragment shader

  VkSampler wraps the view and defines filtering (LINEAR) and wrapping (REPEAT)
Objectives
  • 64×64 RGBA texture data generated procedurally in CPU memory
  • Staging buffer uploaded to DEVICE_LOCAL VkImage via vkCmdCopyBufferToImage
  • Two image layout transitions with correct pipeline barriers
  • VkImageView and VkSampler created
  • Descriptor layout updated: binding 0 = UBO, binding 1 = COMBINED_IMAGE_SAMPLER
  • Pool updated to include sampler descriptor count
  • Fragment shader samples texture and multiplies result
  • ✓ Lab 3 Deliverable
    Rotating Triangle with UBO + Texture Sampler
    Single descriptor set binding two resource types (UBO + sampler). Triangle rotates and is tinted by the procedural texture. All image layout transitions correct with validation: 0 errors.
    • Texture colours visible on the triangle (not white/black)
    • Rotation still working from Lab 1 UBO
    • Can explain the two image layout transitions and why each is needed
    Lab 4 of 5 ⬛ Advanced · ~50 min
    C# P/Invoke Bridge — Command Console to Vulkan Renderer
    Compile the Day 5 Lab 1 renderer as a native Windows DLL. Write a thin C# console application that loads it with P/Invoke, calls an exported StartRenderer() function in a background thread, and sends rotation speed commands via an exported SetRotationSpeed(float) function. The Vulkan window responds in real time. This is the integration pattern used in naval CIC systems where a command layer (C#/.NET) drives a GPU renderer (C++).
    📋 Concepts: __declspec(dllexport) · P/Invoke · DllImport · Thread safety · std::atomic ⚡ Builds on Lab 1 Vulkan renderer
    ➕ What You Build
    C++ DLL exports (add to Lab 1 main.cpp)
    DLL export additions — add to top of main.cpp
    C++
    // Thread-safe rotation speed shared between DLL thread and C# caller
    #include <atomic>
    static std::atomic<float> g_rotSpeed{45.0f};
    
    // Export these two functions for C# P/Invoke
    extern "C" {
        __declspec(dllexport) void StartRenderer() {
            // Call the existing render loop (blocks until ESC or window closed)
            // In the render loop, replace the hardcoded 45.0f with g_rotSpeed.load()
            runRenderLoop();  // refactor main() render loop into this function
        }
    
        __declspec(dllexport) void SetRotationSpeed(float degreesPerSecond) {
            g_rotSpeed.store(degreesPerSecond);
        }
    }
    
    // In CMakeLists.txt — change add_executable to:
    // add_library(NavalRenderer SHARED src/main.cpp)
    // add_definitions(-DBUILDING_DLL)
    C# console — P/Invoke command layer
    Program.cs — C# Command Console
    C#
    // ═══════════════════════════════════════════════════════════════════════════
    //  RR GRAPHICS LAB — DAY 5 · LAB 4
    //  Maritime Tactical System — C# Command Console Bridge
    //  By Raushan Ranjan (MCT) | Koenig Original AI-Courseware
    //
    //  This C# console drives the Vulkan renderer DLL via P/Invoke.
    //  The renderer runs in a background task. This thread sends commands.
    //  Real CIC systems follow this exact pattern: .NET command layer + C++ GPU renderer.
    // ═══════════════════════════════════════════════════════════════════════════
    
    using System;
    using System.Runtime.InteropServices;
    using System.Threading.Tasks;
    
    class TacticalConsole
    {
        // P/Invoke declarations — must match __declspec(dllexport) names exactly
        [DllImport("NavalRenderer.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern void StartRenderer();
    
        [DllImport("NavalRenderer.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern void SetRotationSpeed(float degreesPerSecond);
    
        static void Main(string[] args)
        {
            Console.WriteLine("=== Maritime Tactical Command Console ===");
            Console.WriteLine("Starting Vulkan renderer in background thread...\n");
    
            // Start the Vulkan renderer in a background task
            // StartRenderer() blocks until the window is closed
            var rendererTask = Task.Run(() => StartRenderer());
    
            Console.WriteLine("Renderer running. Commands:");
            Console.WriteLine("  S = Slow (10 deg/sec)");
            Console.WriteLine("  F = Fast (180 deg/sec)");
            Console.WriteLine("  N = Normal (45 deg/sec)");
            Console.WriteLine("  Q = Quit");
            Console.WriteLine("  Any number = Set exact speed (e.g. '90')");
            Console.WriteLine();
    
            while (!rendererTask.IsCompleted)
            {
                if (!Console.KeyAvailable) { Task.Delay(50).Wait(); continue; }
                
                string input = Console.ReadLine()?.Trim() ?? "";
    
                if (string.IsNullOrEmpty(input)) continue;
    
                switch (input.ToUpper())
                {
                    case "S":
                        SetRotationSpeed(10.0f);
                        Console.WriteLine("[CMD] Speed: 10 deg/sec (slow)");
                        break;
                    case "F":
                        SetRotationSpeed(180.0f);
                        Console.WriteLine("[CMD] Speed: 180 deg/sec (fast)");
                        break;
                    case "N":
                        SetRotationSpeed(45.0f);
                        Console.WriteLine("[CMD] Speed: 45 deg/sec (normal)");
                        break;
                    case "Q":
                        Console.WriteLine("[CMD] Quit — close the Vulkan window to exit.");
                        break;
                    default:
                        if (float.TryParse(input, out float speed))
                        {
                            SetRotationSpeed(speed);
                            Console.WriteLine($"[CMD] Speed set to {speed} deg/sec");
                        }
                        else
                        {
                            Console.WriteLine("[ERR] Unknown command. Use S/F/N/Q or a number.");
                        }
                        break;
                }
            }
            Console.WriteLine("Renderer closed. Console exit.");
        }
    }
    Objectives
    ✓ Lab 4 Deliverable
    C# Console Driving Vulkan Renderer
    Two-window system: Vulkan render window + C# command console. Speed changes applied instantly via P/Invoke without restarting the renderer.
    Lab 5 of 5 — Capstone ⬛ Capstone · ~60 min + 10-min live demo
    Maritime Tactical System — Full Capstone Demo
    Bring everything together into a 10-minute live demo covering the full 5-day journey from OpenGL triangle to Vulkan compute pipeline to C# integration. You choose what to demonstrate from the items below. The goal is to show understanding, not just working code — the instructor will ask questions on any component.
    ⏱ 10-min live demo + Q&A All 15 modules covered No new code required
    ✅ Capstone Checklist — All 15 Modules
    🎯 Suggested 10-Minute Demo Flow
    0:00–1:00 — Open Day 1 Lab 5 (animated contact display). Briefly explain what changed from Lab 1 to Lab 5.
    1:00–2:30 — Open Day 2 Lab 3 (textured lit vessel). Change shininess live. Disable texture to show Phong alone.
    2:30–4:00 — Open Day 3 Lab 3 (FBO sonar) or Lab 4 (stencil outline). Toggle effect on/off.
    4:00–5:30 — Run Demo 18 (Vulkan triangle). Explain the 800-line cost vs OpenGL 80 lines. What did each extra line do?
    5:30–7:30 — Run Day 5 Lab 1 (UBO rotation). Change rotation speed by editing the UBO update line. Run Lab 2 terminal to show sonar processing throughput.
    7:30–9:00 — Run Day 5 Lab 4 (C# bridge). Send S/F/90 commands from console. Demonstrate real-time response.
    9:00–10:00 — Q&A. Instructor picks any component and asks how it works.
    Instructor Q&A Bank — Be Ready for These
    ✓ Final Capstone Deliverable — Day 5 Complete
    Maritime Tactical System — 5-Day Journey Complete
    A 10-minute live demonstration covering at least one artefact from each of the 15 modules, with answers to instructor questions on any component demonstrated.
    ← Day 4 Labs
    By Raushan Ranjan (MCT | Educator)
    Koenig Original AI-Courseware · Day 5 Complete · Course Complete
    Module Notes →