Merge pull request 'fix(vulkan): clear startup validation errors on native triangle' (#6) from claude/issue-5 into master

This commit is contained in:
catbot 2026-05-31 22:59:47 +02:00
commit 26a41ac528
3 changed files with 41 additions and 94 deletions

View file

@ -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<VkLayerProperties> 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);

View file

@ -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];
}

View file

@ -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<bool, numFrames> imageInitialised{};
std::thread thread;
VkCommandBuffer drawCmdBuffers[numFrames];
VkSubmitInfo submitInfo;