Vulkan validation errors: per-frame acquire-barrier dstAccessMask/stage mismatch + setup-path cmd-buffer/buffer lifecycle #153

Closed
opened 2026-06-18 18:40:09 +02:00 by catbot · 0 comments
Member

Summary

A native Vulkan build with the validation layer enabled spams validation errors every frame and during scene/resource setup. Reproduced on Crafter.Graphics-de99a5fdd55628a1 driving the 3DForts consumer (3DForts/3DForts#202), NVIDIA RTX 4090, driver 610.43.02, Khronos validation layer 1.4.350. All of the errors below originate in Crafter.Graphics; the consumer has no way to influence them.

There are three distinct problems.


1. Per-frame error: acquire barrier dstAccessMask is inconsistent with its stage mask (the spammy one)

Every single frame the layer logs:

vkCmdPipelineBarrier(): pImageMemoryBarriers[0].dstAccessMask (VK_ACCESS_2_TRANSFER_WRITE_BIT)
is not supported by stage mask (VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT).
(VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820)

Root cause

acquireBarrier (the swapchain UNDEFINED/PRESENT_SRC → GENERAL transition at the top of Window::Render) hardcodes both access bits:

implementations/Crafter.Graphics-Window.cpp:490

.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,

but the barrier is submitted with the per-pass stage union as its dst stage mask:

implementations/Crafter.Graphics-Window.cpp:836,840

const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes);
...
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer],
    VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, swapWriterStages,
    0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);

For an all-compute frame (every menu frame, and the typical game frame — passes only override SwapchainStage() to COMPUTE_SHADER), swapWriterStages == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT. VK_ACCESS_TRANSFER_WRITE_BIT is not a supported access type for the compute stage, so VUID-02820 fires — once per frame, forever.

This is the same inconsistency the surrounding code was careful to avoid for the inter-pass and present barriers (those derive stages from SwapchainStageUnion / SwapchainStage() and only use SHADER_* access). The acquire barrier's dstAccessMask was never narrowed to match.

Proposed fix

Derive acquireBarrier.dstAccessMask from the same swapWriterStages union, patched per frame alongside oldLayout/image (right after swapWriterStages is computed at line 836, before the vkCmdPipelineBarrier at line 840):

// dstAccessMask must only contain access flags supported by the dst stage
// mask (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). The swapchain
// image is written as a storage image by compute/RT passes (SHADER_WRITE) and
// only via TRANSFER_WRITE when a transfer-stage pass is present, so derive the
// access mask from the same per-pass stage union instead of hardcoding both.
VkAccessFlags acquireDstAccess = 0;
if (swapWriterStages & (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
                      | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR))
    acquireDstAccess |= VK_ACCESS_SHADER_WRITE_BIT;
if (swapWriterStages & VK_PIPELINE_STAGE_TRANSFER_BIT)
    acquireDstAccess |= VK_ACCESS_TRANSFER_WRITE_BIT;
acquireBarrier.dstAccessMask = acquireDstAccess;

(The hardcoded init at line 490 can then drop the bits, or be left as a harmless default since it's overwritten each frame.)

Note: the present barrier at line 502 has the mirror-image latent inconsistency — srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT with srcStageMask = swapWriterStages. It doesn't currently fire (SHADER_WRITE is valid for COMPUTE/RT), but if a transfer-stage pass ever writes the image its src access would be missing TRANSFER_WRITE. Worth applying the same derivation for symmetry.


2. Command-buffer / buffer lifecycle errors during scene/resource setup (fired once per match start)

When the consumer enters a Game scene (Lobby_HostStart/Lobby_Start, which creates map meshes / RT acceleration structures / uploads), the layer logs the following burst, all on the same command buffer handle:

vkDestroyBuffer(): can't be called on VkBuffer 0x...2cc that is currently in use by VkCommandBuffer 0x...cc6870.   (x3, three different buffers)
vkBeginCommandBuffer(): on active VkCommandBuffer 0x...cc6870 before it has completed.                            (VUID-vkBeginCommandBuffer-commandBuffer-00049)
vkQueueSubmit(): pSubmits[0].pCommandBuffers[0] VkCommandBuffer 0x...cc6870 is already in use and is not marked for simultaneous use. (VUID-vkQueueSubmit-pCommandBuffers-00071)

The only vkQueueSubmit call sites in the library are in Crafter.Graphics-Window.cpp (the per-frame render loop and the screenshot path), so the offending command buffer is one of drawCmdBuffers[]. The reports say it was re-begun and re-submitted while still pending, and that three buffers were destroyed (immediately, not via the #101 deferred-deletion queue) while that submission was still in flight.

This looks like a missing fence wait on the resource-setup path that runs synchronously during a scene transition (outside the normal Render() fence gate at Window.cpp:808), and/or an immediate Buffer::Clear() where a DeferredClear() is required. I couldn't pin the exact site from the consumer side — it needs someone with the command-buffer/setup architecture in hand. Reproduction is deterministic: it fires once on the first match start, every run.


## Summary A native Vulkan build with the validation layer enabled spams validation **errors** every frame and during scene/resource setup. Reproduced on `Crafter.Graphics-de99a5fdd55628a1` driving the 3DForts consumer (`3DForts/3DForts#202`), NVIDIA RTX 4090, driver 610.43.02, Khronos validation layer 1.4.350. All of the errors below originate in Crafter.Graphics; the consumer has no way to influence them. There are three distinct problems. --- ## 1. Per-frame error: acquire barrier `dstAccessMask` is inconsistent with its stage mask (the spammy one) Every single frame the layer logs: ``` vkCmdPipelineBarrier(): pImageMemoryBarriers[0].dstAccessMask (VK_ACCESS_2_TRANSFER_WRITE_BIT) is not supported by stage mask (VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT). (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820) ``` ### Root cause `acquireBarrier` (the swapchain UNDEFINED/PRESENT_SRC → GENERAL transition at the top of `Window::Render`) hardcodes both access bits: `implementations/Crafter.Graphics-Window.cpp:490` ```cpp .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, ``` but the barrier is submitted with the **per-pass** stage union as its dst stage mask: `implementations/Crafter.Graphics-Window.cpp:836,840` ```cpp const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes); ... vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, swapWriterStages, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier); ``` For an all-compute frame (every menu frame, and the typical game frame — passes only override `SwapchainStage()` to `COMPUTE_SHADER`), `swapWriterStages == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT`. `VK_ACCESS_TRANSFER_WRITE_BIT` is **not** a supported access type for the compute stage, so VUID-02820 fires — once per frame, forever. This is the same inconsistency the surrounding code was careful to avoid for the inter-pass and present barriers (those derive stages from `SwapchainStageUnion` / `SwapchainStage()` and only use `SHADER_*` access). The acquire barrier's `dstAccessMask` was never narrowed to match. ### Proposed fix Derive `acquireBarrier.dstAccessMask` from the same `swapWriterStages` union, patched per frame alongside `oldLayout`/`image` (right after `swapWriterStages` is computed at line 836, before the `vkCmdPipelineBarrier` at line 840): ```cpp // dstAccessMask must only contain access flags supported by the dst stage // mask (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). The swapchain // image is written as a storage image by compute/RT passes (SHADER_WRITE) and // only via TRANSFER_WRITE when a transfer-stage pass is present, so derive the // access mask from the same per-pass stage union instead of hardcoding both. VkAccessFlags acquireDstAccess = 0; if (swapWriterStages & (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR)) acquireDstAccess |= VK_ACCESS_SHADER_WRITE_BIT; if (swapWriterStages & VK_PIPELINE_STAGE_TRANSFER_BIT) acquireDstAccess |= VK_ACCESS_TRANSFER_WRITE_BIT; acquireBarrier.dstAccessMask = acquireDstAccess; ``` (The hardcoded init at line 490 can then drop the bits, or be left as a harmless default since it's overwritten each frame.) Note: the **present** barrier at line 502 has the mirror-image latent inconsistency — `srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT` with `srcStageMask = swapWriterStages`. It doesn't currently fire (SHADER_WRITE is valid for COMPUTE/RT), but if a transfer-stage pass ever writes the image its src access would be missing `TRANSFER_WRITE`. Worth applying the same derivation for symmetry. --- ## 2. Command-buffer / buffer lifecycle errors during scene/resource setup (fired once per match start) When the consumer enters a Game scene (`Lobby_HostStart`/`Lobby_Start`, which creates map meshes / RT acceleration structures / uploads), the layer logs the following burst, all on the **same** command buffer handle: ``` vkDestroyBuffer(): can't be called on VkBuffer 0x...2cc that is currently in use by VkCommandBuffer 0x...cc6870. (x3, three different buffers) vkBeginCommandBuffer(): on active VkCommandBuffer 0x...cc6870 before it has completed. (VUID-vkBeginCommandBuffer-commandBuffer-00049) vkQueueSubmit(): pSubmits[0].pCommandBuffers[0] VkCommandBuffer 0x...cc6870 is already in use and is not marked for simultaneous use. (VUID-vkQueueSubmit-pCommandBuffers-00071) ``` The only `vkQueueSubmit` call sites in the library are in `Crafter.Graphics-Window.cpp` (the per-frame render loop and the screenshot path), so the offending command buffer is one of `drawCmdBuffers[]`. The reports say it was re-begun and re-submitted while still pending, and that three buffers were destroyed (immediately, not via the #101 deferred-deletion queue) while that submission was still in flight. This looks like a missing fence wait on the resource-setup path that runs synchronously during a scene transition (outside the normal `Render()` fence gate at `Window.cpp:808`), and/or an immediate `Buffer::Clear()` where a `DeferredClear()` is required. I couldn't pin the exact site from the consumer side — it needs someone with the command-buffer/setup architecture in hand. Reproduction is deterministic: it fires once on the first match start, every run. ---
jorijnvdgraaf changed title from Vulkan validation errors: per-frame acquire-barrier dstAccessMask/stage mismatch + setup-path cmd-buffer/buffer lifecycle + forced GPU-AV to Vulkan validation errors: per-frame acquire-barrier dstAccessMask/stage mismatch + setup-path cmd-buffer/buffer lifecycle 2026-06-18 19:36:57 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics#153
No description provided.