From cac433ee0965376af90700deff0cafc7e0929a37 Mon Sep 17 00:00:00 2001 From: catbot Date: Sun, 31 May 2026 20:59:10 +0000 Subject: [PATCH] fix(vulkan): clear startup validation errors on native triangle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Vulkan validation errors fired on startup of every native (Vulkan) example, reported in #5: 1. vkCreateDevice enabledLayerCount != 0. Device layers are deprecated and ignored since Vulkan 1.0; passing them is a spec violation (VUID-VkDeviceCreateInfo-enabledLayerCount-12384). The device-layer enumeration/match block in Device::Initialize is removed and enabledLayerCount is pinned to 0 — layers are enabled at the instance only. 2. vkQueueSubmit layout transition on a presentable image that "has not been acquired". StartInit() and RecreateSwapchainAndImages() eagerly transitioned every swapchain image UNDEFINED -> PRESENT_SRC_KHR before any vkAcquireNextImageKHR, which the spec forbids (a presentable image may only be touched after acquire). Those pre-transitions are removed. Each image's first layout transition now happens lazily in Render(), after acquire, from UNDEFINED; subsequent frames transition from PRESENT_SRC_KHR. A per-image `imageInitialised` flag (reset in CreateSwapchain) selects the correct oldLayout. Verified under sway (headless, GPU renderer) + VK_LAYER_KHRONOS_validation: the original code reproduces both errors on HelloUI; the fixed build emits zero validation messages across initial render and swapchain recreation. Resolves #5 Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Device.cpp | 29 ++---- implementations/Crafter.Graphics-Window.cpp | 98 ++++++--------------- interfaces/Crafter.Graphics-Window.cppm | 8 ++ 3 files changed, 41 insertions(+), 94 deletions(-) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index f5ef4a2..5437bf2 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -721,30 +721,11 @@ void Device::Initialize() { deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data(); deviceCreateInfo.pNext = &physical_features2; - uint32_t deviceLayerCount; - CheckVkResult(vkEnumerateDeviceLayerProperties(physDevice, &deviceLayerCount, NULL)); - - std::vector deviceLayerProperties(deviceLayerCount); - CheckVkResult(vkEnumerateDeviceLayerProperties(physDevice, &deviceLayerCount, deviceLayerProperties.data())); - - size_t foundDeviceLayers = 0; - - for (uint32_t i = 0; i < deviceLayerCount; i++) - { - for (size_t j = 0; j < sizeof(layerNames) / sizeof(const char*); j++) - { - if (std::strcmp(deviceLayerProperties[i].layerName, layerNames[j]) == 0) - { - foundDeviceLayers++; - } - } - } - - if (foundDeviceLayers >= sizeof(layerNames) / sizeof(const char*)) - { - deviceCreateInfo.enabledLayerCount = sizeof(layerNames) / sizeof(const char*); - deviceCreateInfo.ppEnabledLayerNames = layerNames; - } + // Device layers are deprecated and have been ignored since Vulkan 1.0; + // enabling them is a validation error. Layers are enabled at instance + // creation only, so leave enabledLayerCount at 0. + deviceCreateInfo.enabledLayerCount = 0; + deviceCreateInfo.ppEnabledLayerNames = nullptr; CheckVkResult(vkCreateDevice(physDevice, &deviceCreateInfo, NULL, &device)); vkGetDeviceQueue(device, queueFamilyIndex, 0, &queue); diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index cec9951..18fe84c 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -451,56 +451,13 @@ void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) { } void Window::RecreateSwapchainAndImages() { + // CreateSwapchain leaves the new swapchain images in + // VK_IMAGE_LAYOUT_UNDEFINED and resets imageInitialised. The images are + // NOT transitioned here: a presentable image may only be touched after + // vkAcquireNextImageKHR returns it, so Render() performs each image's + // first layout transition lazily (from UNDEFINED) once it has been + // acquired. Pre-transitioning unacquired images is a validation error. CreateSwapchain(); - - // CreateSwapchain leaves new swapchain images in VK_IMAGE_LAYOUT_UNDEFINED. - // Render() barriers from PRESENT_SRC_KHR, so transition them now to - // match. Mirrors the StartInit logic, on a one-shot command buffer. - { - VkCommandBufferAllocateInfo cba{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .commandPool = Device::commandPool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1, - }; - VkCommandBuffer cmd = VK_NULL_HANDLE; - Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cba, &cmd)); - - VkCommandBufferBeginInfo cbi{ - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - }; - Device::CheckVkResult(vkBeginCommandBuffer(cmd, &cbi)); - - VkImageSubresourceRange range{ - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }; - for (std::uint32_t i = 0; i < numFrames; i++) { - image_layout_transition(cmd, - images[i], - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - 0, 0, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - range); - } - - Device::CheckVkResult(vkEndCommandBuffer(cmd)); - - VkSubmitInfo si{ - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .commandBufferCount = 1, - .pCommandBuffers = &cmd, - }; - Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &si, VK_NULL_HANDLE)); - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); - } } void Window::SetTitle(const std::string_view title) { @@ -724,11 +681,20 @@ void Window::Render() { range.baseArrayLayer = 0; range.layerCount = VK_REMAINING_ARRAY_LAYERS; + // On an image's first use after (re)creating the swapchain it is still in + // VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in + // PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation + // error, so pick based on whether this image has been rendered before. + // The image was just acquired above, so touching it here is legal. + const bool firstUse = !imageInitialised[currentBuffer]; + imageInitialised[currentBuffer] = true; + VkImageMemoryBarrier image_memory_barrier { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = 0, .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + .oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, @@ -980,6 +946,11 @@ void Window::CreateSwapchain() // Get the swap chain images Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, images)); + // Brand-new swapchain images are in VK_IMAGE_LAYOUT_UNDEFINED; none have + // been rendered/presented yet. Render() consults this to pick the correct + // oldLayout for each image's first post-acquire transition. + imageInitialised.fill(false); + for (std::uint8_t i = 0; i < numFrames; i++) { imageViews[i] = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, @@ -1009,26 +980,13 @@ VkCommandBuffer Window::StartInit() { cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo)); - VkImageSubresourceRange range{}; - range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - range.baseMipLevel = 0; - range.levelCount = VK_REMAINING_MIP_LEVELS; - range.baseArrayLayer = 0; - range.layerCount = VK_REMAINING_ARRAY_LAYERS; - - for(std::uint32_t i = 0; i < numFrames; i++) { - image_layout_transition(drawCmdBuffers[currentBuffer], - images[i], - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - 0, - 0, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - range - ); - } - + // The swapchain images are deliberately NOT transitioned here. They are + // presentable images and may only be touched after vkAcquireNextImageKHR + // returns them; transitioning an unacquired presentable image is a + // validation error. Render() instead transitions each image from + // VK_IMAGE_LAYOUT_UNDEFINED on its first acquired use (see + // imageInitialised). This command buffer is for the caller's one-time + // setup work (uploads, acceleration-structure builds, etc.). return drawCmdBuffers[currentBuffer]; } diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 5113589..2d4b9bd 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -239,6 +239,14 @@ export namespace Crafter { VkColorSpaceKHR colorSpace; VkImage images[numFrames]; VkImageViewCreateInfo imageViews[numFrames]; + // Tracks whether each swapchain image has been rendered (and thus + // left in PRESENT_SRC_KHR) at least once. Freshly created swapchain + // images start in VK_IMAGE_LAYOUT_UNDEFINED, so the first per-frame + // barrier must transition from UNDEFINED, not PRESENT_SRC_KHR. + // Reset in CreateSwapchain(). A presentable image may only be touched + // after it has been acquired, so this initial transition happens + // lazily in Render() (post-acquire) rather than up front. + std::array imageInitialised{}; std::thread thread; VkCommandBuffer drawCmdBuffers[numFrames]; VkSubmitInfo submitInfo;