Fix Vulkan validation errors #92

Merged
Ghost merged 7 commits from AsperTheDog/Ryubing:fix-validation-errors into master 2026-05-15 15:36:15 +00:00
Contributor

This PR fixes several validation errors caused by invalid Vulkan usage. These validation errors often end up invoking Undefined Behavior on the driver side, which can lead to artifacts or crashes which are driver specific and otherwise incredibly hard to track. I don't think it should have any impact on performance, but it would be good to test it with as many games as possible (maybe a bug in a game was fixed?).

Each commit fixes an error. I added to each a description with the validation error that was fixed and a small explanation on what was causing it and how I fixed it.

This PR fixes several validation errors caused by invalid Vulkan usage. These validation errors often end up invoking Undefined Behavior on the driver side, which can lead to artifacts or crashes which are driver specific and otherwise incredibly hard to track. I don't think it should have any impact on performance, but it would be good to test it with as many games as possible (maybe a bug in a game was fixed?). Each commit fixes an error. I added to each a description with the validation error that was fixed and a small explanation on what was causing it and how I fixed it.
```
vkCreateImage(): The following VkImageCreateInfo returned VK_ERROR_FORMAT_NOT_SUPPORTED when calling
  vkGetPhysicalDeviceImageFormatProperties2
  format (VK_FORMAT_BC4_UNORM_BLOCK)
  type (VK_IMAGE_TYPE_2D)
  tiling (VK_IMAGE_TILING_OPTIMAL)
  usage (VK_IMAGE_USAGE_TRANSFER_SRC_BIT|VK_IMAGE_USAGE_TRANSFER_DST_BIT|VK_IMAGE_USAGE_SAMPLED_BIT|VK_IMAGE_USAGE_STORAGE_BIT)
  flags (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT|VK_IMAGE_CREATE_EXTENDED_USAGE_BIT)
  VkImageCreateInfo::pNext is NULL.
  The Vulkan spec states: Each of the following values (as described in Image Creation Limits) must not be undefined : imageCreateMaxMipLevels, imageCreateMaxArrayLayers, imageCreateMaxExtent,
   and imageCreateSampleCounts (https://docs.vulkan.org/spec/latest/chapters/resources.html#VUID-VkImageCreateInfo-imageCreateMaxMipLevels-02251)
```

STORAGE_BIT is being unconditionally set (when VK_IMAGE_CREATE_EXTENDED_USAGE_BIT is being set it overrides the compatibility checks) even on BC-compressed formats, which no Vulkan driver supports for storage. This is probably stemming from a misunderstanding on how VK_IMAGE_CREATE_EXTENDED_USAGE_BIT works. Extended usage relaxes per-format-feature checks for views into the image, but the image itself must still pass vkGetPhysicalDeviceImageFormatProperties2 for format + usage
```
vkCmdPipelineBarrier(): pMemoryBarriers[0].srcAccessMask (VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT) and dstAccessMask (VK_ACCESS_2_SHADER_READ_BIT) is not a subset of subpass dependency's srcAccessMask and dstAccessMask of subpass 0 of VkRenderPass 0x27630000002763.
The Vulkan spec states: If vkCmdPipelineBarrier is called within a render pass instance using a VkRenderPass object, the render pass must have been created with at least one subpass dependency that expresses a dependency from the current subpass to itself, does not include VK_DEPENDENCY_BY_REGION_BIT if this command does not, does not include VK_DEPENDENCY_VIEW_LOCAL_BIT if this command does not, and has synchronization scopes and access scopes that are all supersets of the scopes defined in this command (https://docs.vulkan.org/spec/latest/chapters/synchronization.html#VUID-vkCmdPipelineBarrier-None-07889)
```

Inside a render pass, the renderer sometimes needs to emit a vkCmdPipelineBarrier. Vulkan only allows that in-pass barrier if the render pass was created with a subpass self-dependency whose access/stage scopes are a superset of the barrier's. Our render passes did declare a self-dependency, but its access flags only listed IndexRead | VertexAttributeRead | UniformRead, plus TransformFeedbackWrite when available. The barriers the renderer actually emits in-pass use much wider access flags, I added those access bits to BarrierBatch.GetSubpassAccessSuperset
```
vkCreateImageView(): pCreateInfo->pNext<VkImageViewUsageCreateInfo>.usage (VK_IMAGE_USAGE_SAMPLED_BIT|VK_IMAGE_USAGE_STORAGE_BIT) must not include any bits that were not set in VkImageCreateInfo::usage (VK_IMAGE_USAGE_TRANSFER_SRC_BIT|VK_IMAGE_USAGE_TRANSFER_DST_BIT|VK_IMAGE_USAGE_SAMPLED_BIT|VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT|VK_IMAGE_USAGE_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT) of the image.
The Vulkan spec states: If the pNext chain includes a VkImageViewUsageCreateInfo structure, and image was not created with a VkImageStencilUsageCreateInfo structure included in the pNext chain of VkImageCreateInfo, its usage member must not include any bits that were not set in the usage member of the VkImageCreateInfo structure used to create image (https://docs.vulkan.org/spec/latest/chapters/resources.html#VUID-VkImageViewCreateInfo-pNext-02662)
```

Issue stemming from the fix in the previous commit about storage bit incorrectness. The renderer was creating VkImage and VkImageView with two separate usage computations that could disagree. Before the BC4 fix, the image-side computation always over-claimed STORAGE_BIT, so views were always a subset by accident. After the BC4 fix the image stopped over-claiming, and any view whose format was more storage-capable than the image's format ended up asking for STORAGE_BIT in its VkImageViewUsageCreateInfo without the image ever having had it.

TextureStorage now records the final usage it passed to vkCreateImage in a public UsageFlags property. TextureView computes its own view usage and AND-masks each one with storage.UsageFlags.
```
vkQueueSubmit(): pSubmits[0] command buffer VkCommandBuffer 0x2163b2064b0 expects VkImage 0x27bb00000027bb (subresource: aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, mipLevel = 0, arrayLayer = 0) to be in layout VK_IMAGE_LAYOUT_GENERAL--instead, current layout is VK_IMAGE_LAYOUT_UNDEFINED.
The Vulkan spec states: If a descriptor with type equal to any of VK_DESCRIPTOR_TYPE_SAMPLE_WEIGHT_IMAGE_QCOM, VK_DESCRIPTOR_TYPE_BLOCK_MATCH_IMAGE_QCOM, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, or VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT is accessed as a result of this command, all image subresources identified by that descriptor must be in the image layout identified when the descriptor was written (https://docs.vulkan.org/spec/latest/chapters/drawing.html#VUID-vkCmdDraw-None-09600)
```

Window.Present rents its own command buffer to run scaling-filter and post-process work, then submits it at the end. During that work, the scaling filter lazily creates large intermediate render targets, and each new texture's UNDEFINED to GENERAL barrier got queued onto the renderer's preload command buffer, but nothing was submitting that preload CB before Window's own CB went to the queue. So the queue saw the draw using the texture first, and the layout barrier only after, leaving the validator's tracked layout as UNDEFINED at draw time. I fixed it by calling FlushAllCommands() right before each of Window's submits, so any pending preload work is flushed onto the queue first.
```
pRegions[0].dstOffsets[0].x is 0 and dstOffsets[1].x is 2 which exceed dstSubresource
   width extent (1).

  The VK_IMAGE_TYPE_2D VkImage was created with format VK_FORMAT_R8G8B8A8_SRGB and an extent of [width = 256,
  height = 512, depth = 1]

  &#x09;mipLevel 8 is [width = 1, height = 2, depth = 1]

  The Vulkan spec states: For each element of pRegions, dstOffsets[0].x and dstOffsets[1].x must both be greater than
   or equal to 0 and less than or equal to the width of the specified dstSubresource of dstImage
  (https://docs.vulkan.org/spec/latest/chapters/copies.html#VUID-vkCmdBlitImage-dstOffset-00248)
```

The Switch GPU's texture cache can hand us views whose reported dimensions are larger than the underlying Vulkan storage at the view's mip level. Handled by passing the storage's TextureCreateInfo to TextureCopy.Copy and clamping each copy iteration's extent to the storage's true mip dimensions.
```
 vkCmdBlitImage(): pRegions[0].dstOffsets[0].x is 0 and dstOffsets[1].x is 2 which exceed dstSubresource width extent (1).
  The VK_IMAGE_TYPE_2D VkImage was created with format VK_FORMAT_R8G8B8A8_SRGB and an extent of [width = 256, height = 512, depth = 1]
      mipLevel 8 is [width = 1, height = 2, depth = 1]
  The Vulkan spec states: For each element of pRegions, dstOffsets[0].x and dstOffsets[1].x must both be greater than or equal to 0 and less than or equal to the width of the specified
  dstSubresource of dstImage (https://docs.vulkan.org/spec/latest/chapters/copies.html#VUID-vkCmdBlitImage-dstOffset-00248)
```

Same shape as the prior TextureCopy.Copy clamp. The Switch texture cache hands views whose reported dimensions can exceed the underlying storage at the view's mip level, and vkCmdBlitImage rejects offsets that go past the actual destination mip extents. Fixed by passing the underlying TextureStorage's Info to TextureCopy.Blit and clamping each iteration's computed blit offsets to the storage mip's real dimensions, so well-formed views are unaffected and aliased views get Switch-like out-of-bounds clipping.
fix buffer destroy before submit
All checks were successful
Pull Request Triage / triage (pull_request_target) Successful in 13s
Build PR / linux-arm64 (Release) (pull_request) Successful in 4m28s
Build PR / win-x64 (Release) (pull_request) Successful in 8m56s
Build PR / macOS Universal (Release) (pull_request) Successful in 10m25s
Build PR / linux-x64 (Release) (pull_request) Successful in 18m15s
d868d5d5f4
```
 vkCmdBindVertexBuffers2EXT(): was called in VkCommandBuffer 0x1bc4fb9f890 which is now in an invalid state (instead of recording state) because the following objects bound to the command
  buffer were invalidated
   VkBuffer 0xb164000000b164 was destroyed
  The Vulkan spec states: commandBuffer must be in the recording state (https://docs.vulkan.org/spec/latest/chapters/fxvertex.html#VUID-vkCmdBindVertexBuffers2-commandBuffer-recording)
```

vkCmdBindVertexBuffers was referencing a VkBuffer that got destroyed before submit, because Auto.Get(cbs) ran at BindVertexBuffer time but a flush could move the bind onto a different CB before Commit. Now the updater stores the Auto<DisposableBuffer> and defers Get(cbs) until Commit, so the per-CB dep is registered against the CB that actually receives the bind.
Ghost force-pushed fix-validation-errors from d868d5d5f4
All checks were successful
Pull Request Triage / triage (pull_request_target) Successful in 13s
Build PR / linux-arm64 (Release) (pull_request) Successful in 4m28s
Build PR / win-x64 (Release) (pull_request) Successful in 8m56s
Build PR / macOS Universal (Release) (pull_request) Successful in 10m25s
Build PR / linux-x64 (Release) (pull_request) Successful in 18m15s
to a533996325
All checks were successful
Build PR / win-x64 (Release) (pull_request) Successful in 10m23s
Build PR / macOS Universal (Release) (pull_request) Successful in 10m36s
Build PR / linux-arm64 (Release) (pull_request) Successful in 9m4s
Build PR / linux-x64 (Release) (pull_request) Successful in 10m10s
2026-05-15 15:35:29 +00:00
Compare
Ghost merged commit 58bd19a2f3 into master 2026-05-15 15:36:15 +00:00
Ghost deleted branch fix-validation-errors 2026-05-15 15:36:17 +00:00
Ghost referenced this pull request from a commit 2026-05-15 15:36:17 +00:00
Ghost added this to the 1.4.0 milestone 2026-06-27 17:14:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
projects/Ryubing!92
No description provided.