Project 1

First Light

Boostrapping Vulkan instance, device and swapchain with a post-process pipeline and unexpected issues from the validation layers

Course
CS 599, Ray Traced Real Time Rendering
Submitted
20 September 2026
Status
Complete

Application methods enabled (uncommented)

#CallFileStatus
1createInstance(app->doApiDump)vkapp_init.cppdone
2createPhysicalDevice()vkapp_init.cppdone
3chooseQueueIndex()vkapp_init.cppdone
4createDevice()vkapp_init.cppdone
5getCommandQueue()vkapp_init.cppdone
6loadExtensions()extensions_vk.cppdone
7getSurface()vkapp_init.cppdone
8createSwapchain()vkapp_init.cppdone
9createCommandPool()vkapp_init.cppdone
10createDepthResource()vkapp_postProcess.cppdone
—createScRenderTarget()vkapp_postProcess.cppleft commented (project 2)
—createPostDescriptor()vkapp_postProcess.cppleft commented (project 2)
11createPostPipeline()vkapp_postProcess.cppdone
12drawFrame(): all four phasesvkapp_runtime.cppdone
13renderFrame(): postProcess() onlyvkapp_runtime.cppdone
143 fence/idle lines in destroyAllVulkanResources()vkapp.cppdone

Each section below comes from an incremental build, one step at a time up to the full build that produces the required output of the post-process pipeline. The log from that full run is at the end, under Validation-layer verification.


Initialization

The three-step enumeration

Understand the following three step process for requesting a variable length list from Vulkan. You will use this many more times.

Every list retrieved from Vulkan, from instance layers to surface formats, follows the same three step process:

  1. Ask Vulkan for the count only, passing nullptr for the data pointer.
  2. Create a std::vector<...> to that count.
  3. Call Vulkan again with the vector’s data pointer to have it filled.

Rather than write it out nine times, it is wrapped once in a VK_ENUM_TO_VECTOR macro that takes the element type, the enumerating function, and any leading arguments, and returns the filled vector:

std::vector<VkLayerProperties> availableLayers =
    VK_ENUM_TO_VECTOR(VkLayerProperties, vkEnumerateInstanceLayerProperties);

std::vector<VkSurfaceFormatKHR> formats =
    VK_ENUM_TO_VECTOR(VkSurfaceFormatKHR, vkGetPhysicalDeviceSurfaceFormatsKHR,
                      m_physicalDevice, m_surface);

Both calls in the pair are checked for a successful call output, which is why the log entries show each enumerating function succeeding twice in a row: once for the count, once for the data.

createInstance

(a) GLFW-required instance extensions, appended to reqInstanceExtensions:

[12:26:51.454] info  ---- GLFW required extensions [count : 2] ------------------------------
[12:26:51.454] info  VK_KHR_surface
[12:26:51.454] info  VK_KHR_win32_surface

(b) Available instance layers (vkEnumerateInstanceLayerProperties):

[12:27:45.968] info  ---- InstanceLayers Info [count: 24 ] ----------------------------------
[12:27:45.969] info  VK_LAYER_NV_optimus
[12:27:45.969] info  VK_LAYER_NV_present
[12:27:45.969] info  VK_LAYER_AMD_switchable_graphics
[12:27:45.969] info  GalaxyOverlayVkLayer
[12:27:45.969] info  GalaxyOverlayVkLayer_VERBOSE
[12:27:45.969] info  GalaxyOverlayVkLayer_DEBUG
[12:27:45.969] info  VK_LAYER_EOS_Overlay
[12:27:45.969] info  VK_LAYER_NV_nomad_release_public_2025_4_1
[12:27:45.969] info  VK_LAYER_NV_GPU_Trace_release_public_2025_4_1
[12:27:45.969] info  VK_LAYER_NV_ngfx_capture_release_public_2025_4_1
[12:27:45.970] info  VK_LAYER_NV_shader_debugger_release_public_2025_4_1
[12:27:45.970] info  VK_LAYER_OBS_HOOK
[12:27:45.970] info  VK_LAYER_VALVE_steam_overlay
[12:27:45.970] info  VK_LAYER_VALVE_steam_fossilize
[12:27:45.970] info  VK_LAYER_ROCKSTAR_GAMES_social_club
[12:27:45.970] info  VK_LAYER_LUNARG_api_dump
[12:27:45.970] info  VK_LAYER_LUNARG_gfxreconstruct
[12:27:45.970] info  VK_LAYER_KHRONOS_synchronization2
[12:27:45.970] info  VK_LAYER_KHRONOS_validation
[12:27:45.970] info  VK_LAYER_LUNARG_monitor
[12:27:45.970] info  VK_LAYER_LUNARG_screenshot
[12:27:45.970] info  VK_LAYER_KHRONOS_profiles
[12:27:45.971] info  VK_LAYER_KHRONOS_shader_object
[12:27:45.971] info  VK_LAYER_LUNARG_crash_diagnostic

VK_LAYER_KHRONOS_validation found in the instance layer info: yes.

(c) Available instance extensions (vkEnumerateInstanceExtensionProperties):

[12:34:43.985] info  ---- InstanceExtensions Info [count: 20 ] ------------------------------
[12:34:43.986] info  VK_KHR_device_group_creation
[12:34:43.986] info  VK_KHR_display
[12:34:43.986] info  VK_KHR_external_fence_capabilities
[12:34:43.986] info  VK_KHR_external_memory_capabilities
[12:34:43.987] info  VK_KHR_external_semaphore_capabilities
[12:34:43.987] info  VK_KHR_get_display_properties2
[12:34:43.987] info  VK_KHR_get_physical_device_properties2
[12:34:43.987] info  VK_KHR_get_surface_capabilities2
[12:34:43.987] info  VK_KHR_surface
[12:34:43.988] info  VK_KHR_surface_maintenance1
[12:34:43.988] info  VK_KHR_surface_protected_capabilities
[12:34:43.988] info  VK_KHR_win32_surface
[12:34:43.988] info  VK_EXT_debug_report
[12:34:43.988] info  VK_EXT_debug_utils
[12:34:43.989] info  VK_EXT_direct_mode_display
[12:34:43.989] info  VK_EXT_surface_maintenance1
[12:34:43.989] info  VK_EXT_swapchain_colorspace
[12:34:43.989] info  VK_NV_external_memory_capabilities
[12:34:43.989] info  VK_KHR_portability_enumeration
[12:34:43.990] info  VK_LUNARG_direct_driver_loading
  • Result checked: vkCreateInstance against VK_SUCCESS, throwing std::runtime_error on failure. Verified via the trace log below.
  • Destroy registered: vkDestroyInstance
[12:48:02.018] trace vkCheck successful  vkCreateInstance(&instanceCreateInfo, nullptr, &m_instance)

createPhysicalDevice: chosen and rejected GPUs

This code is in a loop iterating variable physicalDevice through a list of all physicalDevices (GPUs) … and here we judge if the physicalDevice is compatible with our requirements.

GPUdeviceTypeAll reqDeviceExtensions present?Verdict
NVIDIA GeForce RTX 4090DISCRETE_GPUyeschosen
AMD Radeon(TM) GraphicsINTEGRATED_GPUn/arejected, not discrete
[13:25:51.060] info  ---- Phyiscal Devices Info [count :  2] --------------------------------
[13:25:51.060] info  Checking GPU : NVIDIA GeForce RTX 4090
[13:25:51.060] info  Found discrete GPU
[13:25:51.061] trace Checking extensions
[13:25:51.061] info  All required extensions found
[13:25:51.061] info  Checking GPU : AMD Radeon(TM) Graphics
[13:25:51.061] info  Rejecting GPU since its non discrete : [deviceType : VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU]

Selection rule used: a device is compatible iff it is VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU and every entry of reqDeviceExtensions appears in its extension list.

Iterated using an unordered set of available extension names to compare against the required extensions; only one discrete GPU was found.

chooseQueueIndex: queue families

How many queue families does your Vulkan offer? Which of the three flags does each offer? Which of them, by index, has the above three required flags?

Families reported: 6.

IndexGRAPHICSCOMPUTETRANSFERQueue countOther flags
0yesyesyes4VK_QUEUE_SPARSE_BINDING_BIT
1yes2VK_QUEUE_SPARSE_BINDING_BIT
2yesyes3VK_QUEUE_SPARSE_BINDING_BIT
3yes3VK_QUEUE_SPARSE_BINDING_BIT, VK_QUEUE_VIDEO_DECODE_BIT_KHR
4yes3VK_QUEUE_SPARSE_BINDING_BIT, VK_QUEUE_VIDEO_ENCODE_BIT_KHR
5yes3VK_QUEUE_SPARSE_BINDING_BIT, VK_QUEUE_OPTICAL_FLOW_BIT_NV
[13:39:39.795] info  ---- Queue Properties Info : [queue family count : 6] ------------------
[13:39:39.795] info  Required queue flags found at  queueindex : 0, full flag list : VK_QUEUE_GRAPHICS_BIT|VK_QUEUE_COMPUTE_BIT|VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT
[13:39:39.795] info  Required queue flags not found for queueIndex : 1 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT
[13:39:39.795] info  Required queue flags not found for queueIndex : 2 , found : VK_QUEUE_COMPUTE_BIT|VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT
[13:39:39.796] info  Required queue flags not found for queueIndex : 3 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT|VK_QUEUE_VIDEO_DECODE_BIT_KHR
[13:39:39.796] info  Required queue flags not found for queueIndex : 4 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT|VK_QUEUE_VIDEO_ENCODE_BIT_KHR
[13:39:39.796] info  Required queue flags not found for queueIndex : 5 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT|VK_QUEUE_OPTICAL_FLOW_BIT_NV

m_graphicsQueueIndex = 0, the first family with GRAPHICS | COMPUTE | TRANSFER set.

createDevice

The pNext chain is built bottom-up, each structure’s second initializer pointing at the one below it, so features2 ends up as the head of the whole chain:

features2 → features11 → features12 → features13 → accelFeature → rtPipelineFeature → NULL
  • VkPhysicalDeviceRayTracingPipelineFeaturesKHR rtPipelineFeature
  • VkPhysicalDeviceAccelerationStructureFeaturesKHR accelFeature
  • VkPhysicalDeviceVulkan13Features features13
  • VkPhysicalDeviceVulkan12Features features12
  • VkPhysicalDeviceVulkan11Features features11
  • VkPhysicalDeviceFeatures2 features2, passed as deviceCreateInfo.pNext

vkGetPhysicalDeviceFeatures2 fills all six in one call, and features13.dynamicRendering == VK_TRUE is asserted before proceeding.

  • Result checked: vkCreateDevice
  • Destroy registered: vkDestroyDevice
[13:48:07.395] trace vkCheck successful  vkCreateDevice(m_physicalDevice, &deviceCreateInfo, nullptr, &m_device)

getCommandQueue / loadExtensions

Neither step produces any output that needs checking:

  • getCommandQueue: vkGetDeviceQueue returns void, so there is nothing to verify or destroy, since the queue is owned by the device
  • loadExtensions: calls load_VK_EXTENSIONS, a generated function that loads the symbols for every registered extension. No resource is created

Application launch successful, no validation errors.

getSurface

  • Results checked (three): glfwCreateWindowSurface, vkGetPhysicalDeviceSurfaceSupportKHR, and isSupported == VK_TRUE
  • Destroy registered: vkDestroySurfaceKHR
[13:54:48.995] trace vkCheck successful  glfwCreateWindowSurface(m_instance, app->GLFW_window, nullptr, &m_surface)
[13:54:48.997] trace vkCheck successful  vkGetPhysicalDeviceSurfaceSupportKHR(m_physicalDevice, m_graphicsQueueIndex, m_surface, &isSupported)
[13:54:48.997] info  Vulkan supports presenting on GLFW window surface

createSwapchain: present modes and surface formats

Document your PRESENT MODEs. I especially want to know if your system offers VK_PRESENT_MODE_MAILBOX_KHR mode. My high-end windows desktop does; My higher-end Linux laptop doesn’t.

[16:02:15.539] info  ---- Present Mode Info : [count : 5] -----------------------------------
[16:02:15.540] info  VK_PRESENT_MODE_FIFO_KHR
[16:02:15.540] info  VK_PRESENT_MODE_FIFO_RELAXED_KHR
[16:02:15.540] info  Mailbox present mode support found. Mailbox mode will be preferred.
[16:02:15.540] info  VK_PRESENT_MODE_MAILBOX_KHR
[16:02:15.540] info  VK_PRESENT_MODE_IMMEDIATE_KHR
[16:02:15.540] info  VK_PRESENT_MODE_FIFO_LATEST_READY_KHR

VK_PRESENT_MODE_MAILBOX_KHR offered: yes, so MAILBOX is the mode selected.

Then search the formats to choose format VK_FORMAT_B8G8R8A8_UNORM (and its color space) if such exists. Document your list of formats/color-spaces, and your particular choice.

#VkFormatVkColorSpaceKHR
0VK_FORMAT_B8G8R8A8_UNORMVK_COLOR_SPACE_SRGB_NONLINEAR_KHR
1VK_FORMAT_B8G8R8A8_SRGBVK_COLOR_SPACE_SRGB_NONLINEAR_KHR
2VK_FORMAT_R8G8B8A8_UNORMVK_COLOR_SPACE_SRGB_NONLINEAR_KHR
3VK_FORMAT_R8G8B8A8_SRGBVK_COLOR_SPACE_SRGB_NONLINEAR_KHR
4VK_FORMAT_R16G16B16A16_SFLOATVK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT
5VK_FORMAT_A2B10G10R10_UNORM_PACK32VK_COLOR_SPACE_HDR10_ST2084_EXT
6VK_FORMAT_A2B10G10R10_UNORM_PACK32VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[16:02:15.548] info  ---- Surface Format Info : [count : 7] ---------------------------------
[16:02:15.549] info  VK_FORMAT_B8G8R8A8_UNORM
[16:02:15.549] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[16:02:15.549] info  VK_FORMAT_B8G8R8A8_SRGB
[16:02:15.550] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[16:02:15.550] info  VK_FORMAT_R8G8B8A8_UNORM
[16:02:15.551] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[16:02:15.551] info  VK_FORMAT_R8G8B8A8_SRGB
[16:02:15.551] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[16:02:15.552] info  VK_FORMAT_R16G16B16A16_SFLOAT
[16:02:15.552] info  VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT
[16:02:15.552] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[16:02:15.552] info  VK_COLOR_SPACE_HDR10_ST2084_EXT
[16:02:15.553] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[16:02:15.553] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR

Chosen: VK_FORMAT_B8G8R8A8_UNORM / VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, found directly in the list (entry 0), so no fallback to formats[0] is required.

B8G8R8A8 is the most broadly supported native swapchain layout, and the UNORM (rather than _SRGB) variant provides manual control of gamma/tone-mapping in postProcess.frag, avoiding a double gamma-correction when that shader applies its own curve.

The HDR-capable entries (R16G16B16A16_SFLOAT/EXTENDED_SRGB_LINEAR_EXT and A2B10G10R10_UNORM_PACK32/HDR10_ST2084_EXT) are available on this display but kept out of scope for Project 1. They will be considered either as a toggle or as an auto detect mechanism from project 2 onwards.

Other swapchain facts: m_imageCount = capabilities.minImageCount = 2. Window extent taken from capabilities.currentExtent.

[16:02:15.535] info  ---- Capabilites Info --------------------------------------------------
[16:02:15.536] info  Min Image Count : 2
[16:02:15.536] info  Window Extent : Width 1280 , Height 768
  • Results checked: vkCreateSwapchainKHR and vkGetSwapchainImagesKHR, after retrieving the swapchain image list
[16:02:15.847] trace vkCheck successful  vkCreateSwapchainKHR(m_device, &createInfo, nullptr, &m_swapchain)
[16:02:15.848] trace vkCheck successful  vkGetSwapchainImagesKHR
  • Destroy registered: destroySwapchain(), which takes the per-image views and the three sync objects in a 0..m_imageCount loop, then vkDestroySwapchainKHR

createCommandPool

  • Results checked: vkCreateCommandPool and vkAllocateCommandBuffers
  • Destroy registered: vkDestroyCommandPool, which frees the command buffer with it
[16:07:24.183] trace vkCheck successful  vkCreateCommandPool(m_device, &poolCreateInfo, nullptr, &m_cmdPool)
[16:07:24.187] trace vkCheck successful  vkAllocateCommandBuffers(m_device, &allocateInfo, m_commandBuffers.data())

createDepthResource

m_depthImage is an ImageWrap (a small wrapper bundling the image, its memory, view and sampler) built at the current window extent:

ParameterValue
Extentm_windowSize
FormatVK_FORMAT_D32_SFLOAT
UsageVK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
MemoryVK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
AspectVK_IMAGE_ASPECT_DEPTH_BIT
LayoutVK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL
Mip levels1
  • Destroy registered: m_depthImage.destroy(m_device)
  • Resize: destroyed at the top of recreateSizedVulkanResources and recreated at the bottom, since its extent is the window extent

createPostPipeline

Project 1: The best we can do for now … Projects 2 and beyond: What we eventually need.

Per the @@ comment Project 1 uses the first of the two pipeline layouts, because createPostDescriptor() is still commented out and there is no descriptor set layout to point at yet:

// Project 1: the best we can do for now
createInfo.setLayoutCount = 0;
createInfo.pSetLayouts    = nullptr;

// Projects 2 and beyond: what we eventually need
//createInfo.setLayoutCount = 1;
//createInfo.pSetLayouts    = &m_postDesc.descSetLayout;

The push constant range is used in the vertex and fragment stages with the PushConstantPost struct, which has the window attributes width and height. It has been extended to add a time attribute, obtained via glfwGetTime(), for use with the Mandelbulb fractal in the shader.

  • Built with: dynamic rendering, so there is no VkRenderPass
  • Destroy registered: vkDestroyPipeline + vkDestroyPipelineLayout
  • Resize: both destroyed and recreated in recreateSizedVulkanResources

Runtime

  • drawFrame(): all four phases uncommented, in the order acquireFrame(), renderGui(), renderFrame(), submitFrame()
  • renderFrame(): records exactly one command this project, postProcess(). The updateCameraBuffer / raytrace / denoise / rasterize lines stay commented out
  • submitFrame(): vkQueueSubmit and vkQueuePresentKHR results checked
  • Resize: the throw std::runtime_error at the top of recreateSizedVulkanResources was removed and the destroy/recreate pairs added. Resizing rebuilds the swapchain, depth resource and post pipeline against the new extent; the log in the final section shows two such rebuilds (1280×768 → 2560×1369 → 1280×768) with no validation messages. VK_ERROR_OUT_OF_DATE_KHR from vkAcquireNextImageKHR also has been added to the same set of checks.

Debugging note: swapchain acquire timeout and image/semaphore indexing

Two related validation-layer issues surfaced while wiring up acquireFrame() / submitFrame().

(a) vkAcquireNextImageKHR timeout. By default the acquire call was written with a hard-coded 1000000 ns (1 ms) timeout in place of UINT64_MAX:

VkResult result = vkAcquireNextImageKHR(m_device, m_swapchain, 1000000,//UINT64_MAX,
                                        m_imageAvailableSemaphores[currentFrame], ...);

Under normal FIFO/vsync timing an image is frequently not ready within 1 ms, so vkAcquireNextImageKHR returned VK_TIMEOUT.
Per the Vulkan spec, the semaphore is not signaled on VK_TIMEOUT, so the subsequent vkQueueSubmit waited on a semaphore that had no pending signal operation:

Validation Error: [ VUID-vkQueueSubmit-pWaitSemaphores-03238 ]
vkQueueSubmit(): pSubmits[0].pWaitSemaphores[0] queue ... is waiting on semaphore
(VkSemaphore ...[m_imageAvailableSemaphore]) that has no way to be signaled.

Fix: restored UINT64_MAX so acquireFrame() blocks until an image is available.

(b) currentFrame used in place of the acquired image index. After fixing (a), a second pair of errors appeared, this time only under VK_PRESENT_MODE_MAILBOX_KHR:

Validation Error: [ UNASSIGNED-non-acquired-swapchain-image-used ]
vkQueueSubmit(): pSubmits[0] performs a layout transition on presentable VkImage ...,
but the image has not been acquired from VkSwapchainKHR ... (either never or since
the last present operation).

Validation Error: [ VUID-vkQueueSubmit-pSignalSemaphores-00067 ]
vkQueueSubmit(): pSubmits[0].pSignalSemaphores[0] (VkSemaphore ...[m_renderFinishedSemaphore])
is being signaled ..., but it may still be in use by VkSwapchainKHR ...
Most recently acquired image indices: 0, 1, 0, 1, 0, [1], 0, 0.
Swapchain image 1 was presented but was not re-acquired, so VkSemaphore
...[m_renderFinishedSemaphore] may still be in use and cannot be safely reused
with image index 0.

Root cause: postProcess() selected the swapchain image/view by currentFrame (a free-running CPU “frame-in-flight” counter, (currentFrame+1) % m_imageCount) instead of m_swapchainIndex (the actual index returned by vkAcquireNextImageKHR).

The Vulkan spec does not guarantee acquired images come back in round-robin order for any present mode: FIFO only guarantees presentation order, not acquire order. Most drivers seem to behave like round-robin under FIFO with a small image count, which coincidentally kept currentFrame == m_swapchainIndex and masked the bug.

Under MAILBOX the presentation engine is more likely to return images in a different order from the one it was handed (it can drop a queued but unshown image and recycle that slot early), which is what exposed the drift. The same submission then also signaled m_renderFinishedSemaphores[currentFrame] rather than the slot tied to the image actually being presented (represented by m_swapchainIndex), tripping the semaphore reuse check.

Fix: index everything that identifies the physical swapchain image by m_swapchainIndex instead of currentFrame:

  • vkapp_postProcess.cpp: colorAttachment.imageView and both WrapPipelineBarrier calls now use m_imageViews[m_swapchainIndex] / m_swapchainImages[m_swapchainIndex].
  • vkapp_runtime.cpp submitFrame(): pSignalSemaphores and pWaitSemaphores now use m_renderFinishedSemaphores[m_swapchainIndex].

m_imageAvailableSemaphores[currentFrame], m_inFlightFences[currentFrame], and m_commandBuffers[currentFrame] are left indexed by currentFrame. That semaphore is handed to vkAcquireNextImageKHR before the image index is known, so it must stay tied to a rotating frame-in-flight slot rather than image identity, and the fence and command buffer selection are CPU side pacing concerns, not a matter of image identity.


Cleanup and resize

destroyAllVulkanResources

The three fence/idle lines at the top were uncommented as directed: wait on the in-flight fence, reset it, then vkDeviceWaitIdle, so that nothing is still executing when the scope reaches the destroy call for the objects below.

Destruction runs in reverse creation order, and every device-owned object is gone before m_device itself:

vkDestroyPipeline(m_device, m_postPipeline, nullptr);
vkDestroyPipelineLayout(m_device, m_postPipelineLayout, nullptr);
m_depthImage.destroy(m_device);
vkDestroyCommandPool(m_device, m_cmdPool, nullptr);
destroySwapchain();
vkDestroySurfaceKHR(m_instance, m_surface, nullptr);
vkDestroyDevice(m_device, nullptr);
vkDestroyInstance(m_instance, nullptr);

m_surface is destroyed with m_instance as its parent, not m_device, which is why it is added below the device-owned group and above vkDestroyDevice.

destroySwapchain() is its own procedure because it has to loop 0..m_imageCount over the per-image views and the three synchronization objects before destroying the swapchain itself:

for (int idx = 0; idx < m_imageCount; ++idx) {
    vkDestroyImageView(m_device, m_imageViews[idx], nullptr);
    vkDestroyFence(m_device, m_inFlightFences[idx], nullptr);
    vkDestroySemaphore(m_device, m_imageAvailableSemaphores[idx], nullptr);
    vkDestroySemaphore(m_device, m_renderFinishedSemaphores[idx], nullptr);
}
vkDestroySwapchainKHR(m_device, m_swapchain, nullptr);

recreateSizedVulkanResources

Each resized resource needs two entries: a destroy near the top and a recreate near the bottom, on either side of the glfwGetFramebufferSize call that provides the new extent. Three resources qualify for the destroy-recreate pattern, all of them dependent on the window size:

  • Destroyed at the top: m_postPipeline, m_postPipelineLayout, m_depthImage, then destroySwapchain()
  • Recreated at the bottom: createSwapchain(), createDepthResource(), createPostPipeline()

Result

The stock black/red/green/yellow gradient was replaced with a Mandelbulb fractal ported from Shadertoy. The source was adapted to the Vulkan pipeline by extending the existing push constants to carry time from glfwGetTime, alongside the width and height already passed.

Shadertoy source: shadertoy.com/view/3tXXzX

Click to zoom
| First light, the Mandelbulb running through postProcess.frag, driven by glfwGetTime through the post push constants |

Validation-layer verification

VK_LAYER_KHRONOS_validation was requested in reqInstanceLayers and confirmed present in the layer list above. The full session, from startup through the draw loop, two window resizes, and a clean shutdown via Esc, produced no validation-layer messages.

Complete console output of that run:

[18:57:50.510] info  Vulkan SDK version 1.4.357
[18:57:50.511] info  ---- GLFW required extensions [count : 2] ------------------------------
[18:57:50.512] info  VK_KHR_surface
[18:57:50.512] info  VK_KHR_win32_surface
[18:57:50.520] trace vkCheck successful  vkEnumerateInstanceLayerProperties
[18:57:50.526] trace vkCheck successful  vkEnumerateInstanceLayerProperties
[18:57:50.526] info  ---- InstanceLayers Info [count: 24] -----------------------------------
[18:57:50.527] info  VK_LAYER_NV_optimus
[18:57:50.527] info  VK_LAYER_NV_present
[18:57:50.527] info  VK_LAYER_AMD_switchable_graphics
[18:57:50.527] info  GalaxyOverlayVkLayer
[18:57:50.527] info  GalaxyOverlayVkLayer_VERBOSE
[18:57:50.527] info  GalaxyOverlayVkLayer_DEBUG
[18:57:50.527] info  VK_LAYER_EOS_Overlay
[18:57:50.528] info  VK_LAYER_NV_nomad_release_public_2025_4_1
[18:57:50.529] info  VK_LAYER_NV_GPU_Trace_release_public_2025_4_1
[18:57:50.529] info  VK_LAYER_NV_ngfx_capture_release_public_2025_4_1
[18:57:50.529] info  VK_LAYER_NV_shader_debugger_release_public_2025_4_1
[18:57:50.529] info  VK_LAYER_OBS_HOOK
[18:57:50.529] info  VK_LAYER_VALVE_steam_overlay
[18:57:50.529] info  VK_LAYER_VALVE_steam_fossilize
[18:57:50.530] info  VK_LAYER_ROCKSTAR_GAMES_social_club
[18:57:50.530] info  VK_LAYER_LUNARG_api_dump
[18:57:50.530] info  VK_LAYER_LUNARG_gfxreconstruct
[18:57:50.530] info  VK_LAYER_KHRONOS_synchronization2
[18:57:50.530] info  VK_LAYER_KHRONOS_validation
[18:57:50.530] info  VK_LAYER_LUNARG_monitor
[18:57:50.530] info  VK_LAYER_LUNARG_screenshot
[18:57:50.531] info  VK_LAYER_KHRONOS_profiles
[18:57:50.531] info  VK_LAYER_KHRONOS_shader_object
[18:57:50.531] info  VK_LAYER_LUNARG_crash_diagnostic
[18:57:50.534] trace vkCheck successful  vkEnumerateInstanceExtensionProperties
[18:57:50.537] trace vkCheck successful  vkEnumerateInstanceExtensionProperties
[18:57:50.537] info  ---- InstanceExtensions Info [count: 20] -------------------------------
[18:57:50.537] info  VK_KHR_device_group_creation
[18:57:50.537] info  VK_KHR_display
[18:57:50.538] info  VK_KHR_external_fence_capabilities
[18:57:50.538] info  VK_KHR_external_memory_capabilities
[18:57:50.538] info  VK_KHR_external_semaphore_capabilities
[18:57:50.538] info  VK_KHR_get_display_properties2
[18:57:50.538] info  VK_KHR_get_physical_device_properties2
[18:57:50.539] info  VK_KHR_get_surface_capabilities2
[18:57:50.539] info  VK_KHR_surface
[18:57:50.539] info  VK_KHR_surface_maintenance1
[18:57:50.540] info  VK_KHR_surface_protected_capabilities
[18:57:50.540] info  VK_KHR_win32_surface
[18:57:50.540] info  VK_EXT_debug_report
[18:57:50.540] info  VK_EXT_debug_utils
[18:57:50.541] info  VK_EXT_direct_mode_display
[18:57:50.541] info  VK_EXT_surface_maintenance1
[18:57:50.541] info  VK_EXT_swapchain_colorspace
[18:57:50.541] info  VK_NV_external_memory_capabilities
[18:57:50.542] info  VK_KHR_portability_enumeration
[18:57:50.542] info  VK_LUNARG_direct_driver_loading
[18:57:50.656] trace vkCheck successful  vkCreateInstance(&instanceCreateInfo, nullptr, &m_instance)
[18:57:50.658] trace vkCheck successful  vkEnumeratePhysicalDevices
[18:57:50.660] trace vkCheck successful  vkEnumeratePhysicalDevices
[18:57:50.660] info  ---- Phyiscal Devices Info [count :  2] --------------------------------
[18:57:50.661] trace vkCheck successful  vkEnumerateDeviceExtensionProperties
[18:57:50.661] trace vkCheck successful  vkEnumerateDeviceExtensionProperties
[18:57:50.661] info  Checking GPU : NVIDIA GeForce RTX 4090
[18:57:50.661] info  Found discrete GPU
[18:57:50.662] trace Checking extensions
[18:57:50.662] info  All required extensions found
[18:57:50.662] trace vkCheck successful  vkEnumerateDeviceExtensionProperties
[18:57:50.662] trace vkCheck successful  vkEnumerateDeviceExtensionProperties
[18:57:50.662] info  Checking GPU : AMD Radeon(TM) Graphics
[18:57:50.663] info  Rejecting GPU since its non discrete : [deviceType : VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU]
[18:57:50.663] info  ---- Queue Properties Info : [queue family count : 6] ------------------
[18:57:50.663] info  Required queue flags found at  queueindex : 0, full flag list : VK_QUEUE_GRAPHICS_BIT|VK_QUEUE_COMPUTE_BIT|VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT
[18:57:50.663] info  Required queue flags not found for queueIndex : 1 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT
[18:57:50.663] info  Required queue flags not found for queueIndex : 2 , found : VK_QUEUE_COMPUTE_BIT|VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT
[18:57:50.664] info  Required queue flags not found for queueIndex : 3 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT|VK_QUEUE_VIDEO_DECODE_BIT_KHR
[18:57:50.664] info  Required queue flags not found for queueIndex : 4 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT|VK_QUEUE_VIDEO_ENCODE_BIT_KHR
[18:57:50.664] info  Required queue flags not found for queueIndex : 5 , found : VK_QUEUE_TRANSFER_BIT|VK_QUEUE_SPARSE_BINDING_BIT|VK_QUEUE_OPTICAL_FLOW_BIT_NV
[18:57:50.766] trace vkCheck successful  vkCreateDevice(m_physicalDevice, &deviceCreateInfo, nullptr, &m_device)
[18:57:50.767] trace vkCheck successful  glfwCreateWindowSurface(m_instance, app->GLFW_window, nullptr, &m_surface)
[18:57:50.767] trace vkCheck successful  vkGetPhysicalDeviceSurfaceSupportKHR(m_physicalDevice, m_graphicsQueueIndex, m_surface, &isSupported)
[18:57:50.767] info  Vulkan supports presenting on GLFW window surface
[18:57:50.767] info  ---- Capabilites Info --------------------------------------------------
[18:57:50.768] info  Min Image Count : 2
[18:57:50.768] info  Window Extent : Width 1280 , Height 768
[18:57:50.770] trace vkCheck successful  vkGetPhysicalDeviceSurfacePresentModesKHR
[18:57:50.771] trace vkCheck successful  vkGetPhysicalDeviceSurfacePresentModesKHR
[18:57:50.771] info  ---- Present Mode Info : [count : 5] -----------------------------------
[18:57:50.771] info  VK_PRESENT_MODE_FIFO_KHR
[18:57:50.771] info  VK_PRESENT_MODE_FIFO_RELAXED_KHR
[18:57:50.771] info  Mailbox present mode support found. Mailbox mode will be preferred.
[18:57:50.772] info  VK_PRESENT_MODE_MAILBOX_KHR
[18:57:50.772] info  VK_PRESENT_MODE_IMMEDIATE_KHR
[18:57:50.772] info  VK_PRESENT_MODE_FIFO_LATEST_READY_KHR
[18:57:50.777] trace vkCheck successful  vkGetPhysicalDeviceSurfaceFormatsKHR
[18:57:50.779] trace vkCheck successful  vkGetPhysicalDeviceSurfaceFormatsKHR
[18:57:50.780] info  ---- Surface Format Info : [count : 7] ---------------------------------
[18:57:50.780] info  VK_FORMAT_B8G8R8A8_UNORM
[18:57:50.780] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:50.780] info  VK_FORMAT_B8G8R8A8_SRGB
[18:57:50.781] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:50.782] info  VK_FORMAT_R8G8B8A8_UNORM
[18:57:50.782] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:50.782] info  VK_FORMAT_R8G8B8A8_SRGB
[18:57:50.783] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:50.783] info  VK_FORMAT_R16G16B16A16_SFLOAT
[18:57:50.783] info  VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT
[18:57:50.784] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[18:57:50.784] info  VK_COLOR_SPACE_HDR10_ST2084_EXT
[18:57:50.784] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[18:57:50.785] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:51.035] trace vkCheck successful  vkCreateSwapchainKHR(m_device, &createInfo, nullptr, &m_swapchain)
[18:57:51.036] trace vkCheck successful  vkGetSwapchainImagesKHR
[18:57:51.037] trace vkCheck successful  vkGetSwapchainImagesKHR
[18:57:51.037] trace vkCheck successful  vkCreateCommandPool(m_device, &poolCreateInfo, nullptr, &m_cmdPool)
[18:57:51.038] trace vkCheck successful  vkAllocateCommandBuffers(m_device, &allocateInfo, m_commandBuffers.data())
[18:57:51.042] info  ---- Draw loop ---------------------------------------------------------
[18:57:53.670] info  ---- Capabilites Info --------------------------------------------------
[18:57:53.670] info  Min Image Count : 2
[18:57:53.670] info  Window Extent : Width 2560 , Height 1369
[18:57:53.671] trace vkCheck successful  vkGetPhysicalDeviceSurfacePresentModesKHR
[18:57:53.671] trace vkCheck successful  vkGetPhysicalDeviceSurfacePresentModesKHR
[18:57:53.671] info  ---- Present Mode Info : [count : 5] -----------------------------------
[18:57:53.671] info  VK_PRESENT_MODE_FIFO_KHR
[18:57:53.671] info  VK_PRESENT_MODE_FIFO_RELAXED_KHR
[18:57:53.672] info  Mailbox present mode support found. Mailbox mode will be preferred.
[18:57:53.672] info  VK_PRESENT_MODE_MAILBOX_KHR
[18:57:53.672] info  VK_PRESENT_MODE_IMMEDIATE_KHR
[18:57:53.672] info  VK_PRESENT_MODE_FIFO_LATEST_READY_KHR
[18:57:53.674] trace vkCheck successful  vkGetPhysicalDeviceSurfaceFormatsKHR
[18:57:53.676] trace vkCheck successful  vkGetPhysicalDeviceSurfaceFormatsKHR
[18:57:53.676] info  ---- Surface Format Info : [count : 7] ---------------------------------
[18:57:53.677] info  VK_FORMAT_B8G8R8A8_UNORM
[18:57:53.677] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:53.677] info  VK_FORMAT_B8G8R8A8_SRGB
[18:57:53.677] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:53.677] info  VK_FORMAT_R8G8B8A8_UNORM
[18:57:53.678] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:53.678] info  VK_FORMAT_R8G8B8A8_SRGB
[18:57:53.678] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:53.679] info  VK_FORMAT_R16G16B16A16_SFLOAT
[18:57:53.679] info  VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT
[18:57:53.679] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[18:57:53.679] info  VK_COLOR_SPACE_HDR10_ST2084_EXT
[18:57:53.679] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[18:57:53.680] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:53.695] trace vkCheck successful  vkCreateSwapchainKHR(m_device, &createInfo, nullptr, &m_swapchain)
[18:57:53.695] trace vkCheck successful  vkGetSwapchainImagesKHR
[18:57:53.695] trace vkCheck successful  vkGetSwapchainImagesKHR
[18:57:55.772] info  ---- Capabilites Info --------------------------------------------------
[18:57:55.772] info  Min Image Count : 2
[18:57:55.772] info  Window Extent : Width 1280 , Height 768
[18:57:55.772] trace vkCheck successful  vkGetPhysicalDeviceSurfacePresentModesKHR
[18:57:55.773] trace vkCheck successful  vkGetPhysicalDeviceSurfacePresentModesKHR
[18:57:55.773] info  ---- Present Mode Info : [count : 5] -----------------------------------
[18:57:55.773] info  VK_PRESENT_MODE_FIFO_KHR
[18:57:55.773] info  VK_PRESENT_MODE_FIFO_RELAXED_KHR
[18:57:55.773] info  Mailbox present mode support found. Mailbox mode will be preferred.
[18:57:55.774] info  VK_PRESENT_MODE_MAILBOX_KHR
[18:57:55.774] info  VK_PRESENT_MODE_IMMEDIATE_KHR
[18:57:55.774] info  VK_PRESENT_MODE_FIFO_LATEST_READY_KHR
[18:57:55.776] trace vkCheck successful  vkGetPhysicalDeviceSurfaceFormatsKHR
[18:57:55.777] trace vkCheck successful  vkGetPhysicalDeviceSurfaceFormatsKHR
[18:57:55.777] info  ---- Surface Format Info : [count : 7] ---------------------------------
[18:57:55.778] info  VK_FORMAT_B8G8R8A8_UNORM
[18:57:55.778] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:55.778] info  VK_FORMAT_B8G8R8A8_SRGB
[18:57:55.778] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:55.778] info  VK_FORMAT_R8G8B8A8_UNORM
[18:57:55.778] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:55.779] info  VK_FORMAT_R8G8B8A8_SRGB
[18:57:55.779] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:55.779] info  VK_FORMAT_R16G16B16A16_SFLOAT
[18:57:55.779] info  VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT
[18:57:55.779] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[18:57:55.779] info  VK_COLOR_SPACE_HDR10_ST2084_EXT
[18:57:55.780] info  VK_FORMAT_A2B10G10R10_UNORM_PACK32
[18:57:55.780] info  VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
[18:57:55.788] trace vkCheck successful  vkCreateSwapchainKHR(m_device, &createInfo, nullptr, &m_swapchain)
[18:57:55.788] trace vkCheck successful  vkGetSwapchainImagesKHR
[18:57:55.788] trace vkCheck successful  vkGetSwapchainImagesKHR
[18:57:57.964] info  ---- Cleanup -----------------------------------------------------------

Appendix A: where each @@ comment is addressed

@@ requestFileAnswered in
Understand the three-step list processvkapp_init.cppThe three-step enumeration
Append GLFW required extensionsvkapp_init.cppcreateInstance (a)
Print out the availableLayersvkapp_init.cppcreateInstance (b)
Print out the availableExtensionsvkapp_init.cppcreateInstance (c)
Verify success of vkCreateInstancevkapp_init.cppcreateInstance
Judge each physicalDevice for compatibilityvkapp_init.cppcreatePhysicalDevice
How many queue families / which flags / which indexvkapp_init.cppchooseQueueIndex
Build a pNext chain of the six feature structuresvkapp_init.cppcreateDevice
Verify success of vkCreateDevicevkapp_init.cppcreateDevice
Verify glfwCreateWindowSurface, surface support, isSupportedvkapp_init.cppgetSurface
Document your PRESENT MODEs (and MAILBOX)vkapp_init.cppcreateSwapchain
Choose MAILBOX if presentvkapp_init.cppcreateSwapchain
Document the format / color-space list and your choicevkapp_init.cppcreateSwapchain
Verify vkCreateSwapchainKHR and vkGetSwapchainImagesKHRvkapp_init.cppcreateSwapchain
Destroy the swapchain in its own procedurevkapp_init.cppdestroyAllVulkanResources
Verify vkCreateCommandPool / vkAllocateCommandBuffersvkapp_init.cppcreateCommandPool
Project 1 vs. projects 2+ pipeline layoutvkapp_postProcess.cppcreatePostPipeline
Uncomment the four drawFrame phasesvkapp_runtime.cppRuntime
Uncomment postProcess() in renderFramevkapp_runtime.cppRuntime
Uncomment the three fence/idle linesvkapp.cppdestroyAllVulkanResources
Delete the resize throw; destroy/recreate pairsvkapp.cpprecreateSizedVulkanResources
Screen capture of the final result1-first-light.pdfResult: capture of the Mandelbulb replacement
No validation-layer messages1-first-light.pdfValidation-layer verification

Appendix B: destroy / resize registry

All resources, with whether they are destroyed and whether they need recreating on a resize.

ResourceDestroyedResized
m_instanceyes—
m_deviceyes—
m_surfaceyes—
m_swapchain (+ image views, semaphores, fences)yesyes
m_cmdPoolyes—
m_depthImageyesyes
m_postPipelineLayout / m_postPipelineyesyes