Skip to content

Instantly share code, notes, and snippets.

@jimblandy
Last active October 18, 2025 00:22
Show Gist options
  • Save jimblandy/c056a5f5e30f46f072f0320aba299a8c to your computer and use it in GitHub Desktop.
Save jimblandy/c056a5f5e30f46f072f0320aba299a8c to your computer and use it in GitHub Desktop.
Good explanation of Vulkan pipeline barriers from ChatGPT

I'm generally familiar with Vulkan, but I don't quite understand the rules around vkCmdPipelineBarrier, specifically for buffer memory barriers. My understanding is that depending on the source and destination access masks, a pipeline barrier may not always be necessary. Why is this?

ChatGPT:

Short answer: because a barrier only solves hazards, and many access-mask pairs don’t describe a hazard.

In Vulkan, a pipeline barrier does two distinct things:

  1. Execution dependency

    It orders work from srcStageMask → dstStageMask (i.e., “don’t start these later stages until those earlier stages reach a point”).

  2. Memory dependency

    It makes the results of the source writes (srcAccessMask) available, and makes them visible to the destination reads/writes (dstAccessMask) that follow.

A memory dependency is only needed when there’s a hazard on the same memory range. The only true memory hazards are:

  • Read-after-Write (RAW): later reads must see earlier writes → needs memory + execution dependency.
  • Write-after-Read (WAR): later writes must not start until earlier reads are done → needs execution ordering, but no visibility of data is required.
  • Write-after-Write (WAW): order of writes matters → needs execution ordering; visibility is only needed if the second op reads the first op’s data (rare).

By contrast:

  • Read-after-Read (RAR) is not a hazard → no barrier needed.
  • Accesses to disjoint ranges aren’t hazards → no barrier needed.
  • If you already have a dependency another way (semaphore between submissions, a subpass dependency inside a render pass, queue-family transfer with an acquire/release), an extra barrier is redundant.

This ties directly to the access masks:

  • If srcAccessMask doesn’t include a write that actually wrote the bytes you care about, there’s nothing to “make available”.
  • If dstAccessMask doesn’t include a read (or a write that needs ordering like WAR/WAW), there’s nothing to “make visible”.
  • If both masks are read-only (e.g., VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT → VK_ACCESS_SHADER_READ_BIT), that’s RAR → no memory barrier required (and usually no execution barrier either).
  • Sometimes you only need execution ordering (e.g., WAR/WAW): you can set access masks to 0 and just use stages to sequence work, or use the precise access bits but know that visibility isn’t the point.

Concrete examples

  • Transfer write → compute read of same buffer region Hazard: RAW. Use a barrier (e.g., srcStage=TRANSFER, srcAccess=TRANSFER_WRITE; dstStage=COMPUTE_SHADER, dstAccess=SHADER_READ).

  • Vertex shader reads buffer A → fragment shader also reads buffer A RAR, no hazard. No barrier necessary.

  • Compute reads buffer A → later transfer overwrites same region WAR. You need to order them (srcStage=COMPUTE_SHADERdstStage=TRANSFER), but memory visibility isn’t required for correctness; an execution barrier (access masks 0) is sufficient. Many devs still specify access masks out of habit; it doesn’t harm, but it’s not needed for visibility.

  • Two writes to the same region where the second overwrites the first and you don’t care about the first’s value WAW. You need execution ordering so they don’t race; no read visibility is needed.

Practical checklist

  • Do the ranges overlap? If no → no barrier.

  • Is there a write on either side? If no → no barrier.

  • If yes:

    • RAW → execution + memory dependency (src: write access, dst: read access).
    • WAR/WAW → execution ordering only; memory visibility only if the later op also reads prior results.
  • Already have a semaphore/fence/subpass dependency that orders these? If yes → no extra barrier.

(If you use Synchronization2: think in terms of one struct that names the exact stages and access types on both sides. The same rules above apply; it’s just harder to do the wrong thing.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment