Skip to content

fix: synchronise Vulkan attachment reads, buffer barriers and swapchain acquisition - #3324

Draft
sasvdw wants to merge 2 commits into
stride3d:masterfrom
LazyWorksZA:fix/vulkan-attachment-reads-and-buffer-barriers
Draft

fix: synchronise Vulkan attachment reads, buffer barriers and swapchain acquisition#3324
sasvdw wants to merge 2 commits into
stride3d:masterfrom
LazyWorksZA:fix/vulkan-attachment-reads-and-buffer-barriers

Conversation

@sasvdw

@sasvdw sasvdw commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

PR Details

#3311 has merged. This PR now shows its own 2 commits across 9 files.

It overlaps #3337 and #3338, but depends on neither. It edits two of the same files. Whichever merges second needs a rebase.

Vulkan barriers left three classes of access unsynchronized. This PR fixes all three, and adds the switch that reports them.

1. Attachment barriers covered writes but not reads

BarrierMapping.ToVkAccessFlags gave RenderTarget only ColorAttachmentWrite, and DepthStencilWrite only DepthStencilAttachmentWrite. A color attachment is also read, by blending and by a load operation that preserves the existing contents. A writable depth attachment is read by the depth and stencil tests. Every barrier the engine issued for a render target or a depth buffer therefore left the render pass read unsynchronized.

Texture.Vulkan.cs applied the same write-only masks as a texture's initial access state. A fix to the mapping alone was not enough.

Direct3D12 already implements this model. One RENDER_TARGET access bit covers both directions, and DEPTH_STENCIL_WRITE permits the test reads. Vulkan keeps its access flags strictly separate, so the same intent has to be spelled out.

BarrierLayout never documented which accesses each member covers. UnorderedAccess is the one member whose documentation said "reading and writing", and it is also the one member whose mapping was already correct. The two members that were wrong are the two that documented nothing. Each member now states what it covers, so a backend maps against a contract instead of a name.

Synchronization validation reported 40 READ_AFTER_WRITE hazards across 5 tests before this change, and none after.

2. ResourceBarrierTransition threw for buffers

The method handled Texture and threw NotImplementedException for everything else. A compute shader could not hand a structured buffer write to a later reader.

Buffers have no layout in Vulkan, so the new branch derives access and stage flags from BarrierLayout and never calls ToVkImageLayout. It records the last access per command buffer, so a later transition names an accurate source instead of the buffer's static usage superset. On first touch it reads NativeAccessMask and NativePipelineStageMask as a conservative source, but it never writes them. The copy and upload paths need those to stay a superset of every legal usage.

3. Swapchain images were transitioned before acquisition

CreateBackBuffers transitioned every swapchain image to Present and submitted that command buffer before the first acquisition. Vulkan permits use of a presentable image only between vkAcquireNextImageKHR and vkQueuePresentKHR, and a layout transition is such a use. Validation reported the violation once per image per swapchain creation.

The transition existed to give the tracked layout a valid source. AcquireNextImage now sets that state to Undefined, which is correct because the contents of a newly acquired image are undefined. This also removes a vkQueueWaitIdle from every swapchain creation and resize.

This defect needs a real swapchain, so no headless test reaches it. The interactive test runner found it, and the same runner confirms the fix.

Opt-in synchronization validation

STRIDE_VULKAN_SYNC_VALIDATION=1 enables VK_EXT_validation_features with SynchronizationValidation. Core validation does not report a missing barrier. This does, and it is what found the first defect above.

The switch is opt-in for a reason. Synchronization validation also reports hazards that predate any given change, which would fail unrelated tests. Its extension comes from the validation layer rather than the ICD, so an instance extension query made with no layer name never lists it.

Tests

TestBufferBarrier is new. A compute shader writes a structured buffer, and a second dispatch reads that buffer as a shader resource.

The consumer is a dispatch rather than a readback on purpose. A readback copy emits its own barrier from the buffer's access and stage masks. That barrier would synchronize the write even with no transition present, and the test could never fail.

The honest limit of this test: it proves the barrier is emitted and legal, and that the round trip produces the expected values. It does not prove that removing the barrier fails deterministically, because the hazards synchronization validation catches are reported at vkQueueSubmit time and across command buffers. Run the suite with STRIDE_VULKAN_SYNC_VALIDATION=1 to have Vulkan report the hazard directly.

Verification

Debug build, so the validation layers load.

Suite Vulkan / Lavapipe Direct3D11 / WARP Direct3D12 / WARP
Stride.Graphics.Tests.11_0 (7 tests) 6 passed, 1 skipped 6 passed, 1 skipped 6 passed, 1 skipped
Stride.Graphics.Tests.10_0 (48 tests) 44 passed, 4 skipped 44 passed, 4 skipped 44 passed, 4 skipped

Both suites also run clean on Vulkan with STRIDE_VULKAN_SYNC_VALIDATION=1 and report zero hazards. The default path, with the switch unset, is unchanged.

The interactive test runner confirms defect 3. Before the change it logged the acquisition error once per image per swapchain, including after a window resize, which recreates the swapchain. After the change the log is clean and the preview renders correctly.

Direct3D is unaffected by design. Every behavioral change is inside the Vulkan backend. BarrierLayout.cs gains documentation only.

One caveat, carried over from #3311: TestHammersley on Direct3D12 failed twice during this work, both times on the first Direct3D12 run after another backend had run. It then passed 8 consecutive full-suite runs and a run against cleared shader caches, so the trigger is not identified. This PR changes no Direct3D code.

Related Issue

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🤖 Draft PR — automatic CI is skipped to save runner minutes.

  • Mark the PR ready for review to run the full automatic CI — or add a ci-run-on-draft label to run it now without leaving draft.
  • Or arm a specific opt-in suite: ci-enduser, ci-editor, ci-ios, ci-android.

sasvdw and others added 2 commits August 9, 2026 12:27
BarrierMapping.ToVkAccessFlags granted write-only access to attachments:
RenderTarget gave ColorAttachmentWrite and DepthStencilWrite gave
DepthStencilAttachmentWrite. A colour attachment is also read, by blending and
by a load op preserving existing contents, and a writable depth attachment is
read by the depth and stencil tests, so every barrier the engine issued for a
render target or depth buffer left the render pass's read unsynchronised.
Texture.Vulkan.cs applied the same write-only masks as a texture's initial
access state, so correcting the mapping alone was not enough.

This aligns Vulkan with the model Direct3D12 already implements, where a single
RENDER_TARGET access bit covers both and DEPTH_STENCIL_WRITE permits test reads.
Vulkan's access flags are strictly separate, so the same intent has to be spelled
out. BarrierLayout never documented which accesses each member covers, which is
why UnorderedAccess, the one member whose documentation says "reading and
writing", is also the one whose mapping was already correct; the two that were
wrong are the two that said nothing.

ResourceBarrierTransition also threw for anything that was not a Texture, so a
compute write to a structured buffer could not be handed to a later reader.
Buffers have no Vulkan layout, so the new branch derives access and stage from
BarrierLayout without consulting ToVkImageLayout. It reads the buffer's static
access and stage masks as a conservative source but never writes them, because
the copy and upload paths rely on those staying a superset of every legal usage.

Adds opt-in synchronization validation behind STRIDE_VULKAN_SYNC_VALIDATION=1,
which is what surfaced the attachment hazards. Its extension comes from the
validation layer rather than the ICD, so it is absent from an instance extension
query made with no layer name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CreateBackBuffers transitioned every swapchain image to Present and submitted
that command buffer before the first acquisition. Vulkan permits use of a
presentable image only between vkAcquireNextImageKHR and vkQueuePresentKHR,
and a layout transition is such a use. Validation reported the violation once
per image per swapchain creation.

The transition existed to give the tracked layout a valid source. AcquireNextImage
now sets that state to Undefined instead, which is correct because the contents
of a newly acquired image are undefined. This also removes a vkQueueWaitIdle
from every swapchain creation and resize.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sasvdw
sasvdw force-pushed the fix/vulkan-attachment-reads-and-buffer-barriers branch from b3b9a75 to 8bf9313 Compare August 9, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant