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.
- ►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)
- 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
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.
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.
- Triangle rotates smoothly without jitter or stutter
- Validation layers: 0 errors reported
- Can explain: why one UBO per swapchain image, not one shared UBO
- ►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
#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;
}- 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
- 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
- ►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
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)
- 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
- 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