Skip to content

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

Merged
xen2 merged 5 commits into
stride3d:masterfrom
LazyWorksZA:fix/vulkan-attachment-reads-and-buffer-barriers
Sep 20, 2026
Merged

xen2 merged 5 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

Rebased onto master after #3337 (Vulkan 1.3, dynamic rendering) and #3338 (explicit transitions) merged. Two things changed in the rebase: the swapchain block section 3 deletes is now the vkQueueSubmit2 form of the same code, and the VkValidationFeaturesEXT struct is chained ahead of the MoltenVK VkLayerSettingsCreateInfoEXT that #3337 added, so both reach vkCreateInstance on Apple platforms.

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.

Under dynamic rendering the hazard is reported at vkCmdBeginRendering for every attachment with LOAD_OP_LOAD: colour, depth and stencil. Synchronization validation names the missing COLOR_ATTACHMENT_READ and DEPTH_STENCIL_ATTACHMENT_READ bits directly.

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 reports the violation once per image per swapchain creation, including after every resize.

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.

An image tracked as Undefined also exposed a gap in the copy paths. Copy, CopyRegion and UpdateSubResource restore a texture to its tracked layout after the transfer, and Undefined is not a legal restore target. When the tracked layout is Undefined, the transfer layout now becomes the tracked state before the restore barrier is built. GraphicsPresenter.BeginDraw transitions the back buffer to RenderTarget before user code runs, so the game loop never reaches this; a caller that drives the presenter directly could.

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. A follow-up PR adds a CI leg that runs the graphics suites with it. 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. Stride.Graphics.Tests.11_0 and Stride.Graphics.Tests.10_0 pass on Vulkan (Lavapipe), Direct3D11 (WARP) and Direct3D12 (WARP). Both suites also pass on Vulkan with STRIDE_VULKAN_SYNC_VALIDATION=1 and report no hazard. The default path, with the switch unset, is unchanged.

Each defect was checked A/B on the rebased code:

  • Defect 1. With the access-mask fix reverted and synchronization validation on, TestHammersley and TestRadiancePrefilteringGgxFaceContinuity fail on READ_AFTER_WRITE hazards at vkCmdBeginRendering. With the fix, no hazard.
  • Defect 2. TestBufferBarrier passes on all three backends.
  • Defect 3. A windowed game with two mid-run resizes, so three swapchain creations. With master's presenter, validation reports performs a layout transition on presentable VkImage … but the image has not been acquired on vkQueueSubmit2 for every image of every swapchain. With this PR, no error and no warning, with and without synchronization validation.
  • Copy paths. Forcing a texture's tracked layout to Undefined and issuing Copy (as source and as destination), CopyRegion and SetData reports newLayout is VK_IMAGE_LAYOUT_UNDEFINED for each on the previous head. With this PR, the same run is clean, with and without synchronization validation.

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

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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VHYKv129fEUp6sxMbybZWf

@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
sasvdw force-pushed the fix/vulkan-attachment-reads-and-buffer-barriers branch from b3b9a75 to 8bf9313 Compare August 9, 2026 11:11
sasvdw and others added 2 commits September 16, 2026 22:53
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 8bf9313 to 379c78b Compare September 16, 2026 21:40
@sasvdw
sasvdw marked this pull request as ready for review September 16, 2026 21:41
@xen2

xen2 commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Thanks, the Voxels rewrite is right. I pushed two commits on top, please pull before continuing.

- Mixer: a stage compose declared only by a nested shader could not be supplied at the root either. Now a stage slot always takes its value from the root. Two tests added.
- Voxels: marchers bind their own samplers (ApplyAttributeSamplers), so LightVoxelRenderer no longer hard-codes the Marcher path.

Shader tests pass (849). I did not run the voxel GI demo, could you check it once?

@xen2

xen2 commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Sorry, wrong PR for previous message!

Comment thread sources/engine/Stride.Graphics/Vulkan/CommandList.Vulkan.cs
Comment thread sources/engine/Stride.Graphics/Vulkan/GraphicsAdapterFactory.Vulkan.cs Outdated
Comment thread sources/engine/Stride.Graphics.Tests.11_0/TestBufferBarrier.cs
Comment thread sources/engine/Stride.Graphics/Vulkan/GraphicsAdapterFactory.Vulkan.cs Outdated
sasvdw and others added 3 commits September 20, 2026 14:58
…d extension list

The extension is added to the desired set directly, because the validation layer
provides it and an instance extension query made with no layer name never lists it.
Nothing consulted the availability set for it. The list is now a Span, so no count
is repeated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHYKv129fEUp6sxMbybZWf
ToVkPipelineStageFlags(ShaderResource) named only the fragment and compute stages.
A buffer or texture written by compute and read in a vertex shader, as GPU
skinning, instancing and particles do, was not synchronised against that read.
The static masks in Buffer.Vulkan.cs and Texture.Vulkan.cs already include the
vertex shader stage, so the three now agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHYKv129fEUp6sxMbybZWf
…t is Undefined

Copy, CopyRegion and UpdateSubResource move a texture to a transfer layout and
restore it to NativeLayout afterwards. A texture whose tracked layout is Undefined
therefore got a restore barrier with newLayout = VK_IMAGE_LAYOUT_UNDEFINED, which
VUID-VkImageMemoryBarrier-newLayout-01198 forbids. The swapchain image is tracked
as Undefined from acquisition until its first transition.

When the tracked layout is Undefined, the transfer layout now becomes the tracked
state before the restore barrier is built, so that barrier is a same-layout
transfer barrier and the next transition names the transfer layout as its source.

Presenter.BeginDraw transitions the back buffer to RenderTarget before user code
runs, so the game loop never reaches this. A caller that drives the presenter
directly could.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHYKv129fEUp6sxMbybZWf
@xen2 xen2 added ci-enduser Run the end-user sample screenshot + packaging suite on this PR ci-editor Run the GameStudio editor screenshot suite on this PR ci-ios Run the iOS game test suite on this PR ci-android Run the Android game test suite on this PR labels Sep 20, 2026
@xen2
xen2 merged commit de05e91 into stride3d:master Sep 20, 2026
65 of 66 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-android Run the Android game test suite on this PR ci-editor Run the GameStudio editor screenshot suite on this PR ci-enduser Run the end-user sample screenshot + packaging suite on this PR ci-ios Run the iOS game test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants