Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.30.37] - 2026-08-07

### Fixed

- **vulkan:** Align swapchain render pass FinalLayout with Rust wgpu / Dawn (ADR-059)
- Render passes for swapchain images now use `COLOR_ATTACHMENT_OPTIMAL` instead of `PRESENT_SRC_KHR` as FinalLayout
- Explicit barrier `COLOR_ATTACHMENT_OPTIMAL → PRESENT_SRC_KHR` in `ensurePresentLayout()` before present
- Eliminated synchronous `vkWaitForFences` GPU stall — barrier submit uses no fence, command pool reset deferred to next `acquireNextImage()` where acquire fence guarantees completion
- Matches Rust wgpu (`device.rs:119-120`) and Dawn (`RenderPassCache.cpp:173-174`) — both use `InitialLayout == FinalLayout == COLOR_ATTACHMENT_OPTIMAL`
- Removes unnecessary PRESENT_SRC_KHR round-trip on multi-pass frames (TBDR: no extra decompress/compress cycle)

## [0.30.36] - 2026-08-06

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions hal/vulkan/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,14 +671,14 @@ func (e *CommandEncoder) BeginRenderPass(desc *hal.RenderPassDescriptor) hal.Ren
// with explicit transition to ShaderReadOnlyOptimal; until then, General is
// safe for both color-attachment writes and shader reads.
// Reference: Rust wgpu derive_image_layout() uses General for mixed usage.
colorFinalLayout := vk.ImageLayoutPresentSrcKhr // Default for swapchain
colorFinalLayout := vk.ImageLayoutColorAttachmentOptimal // ADR-059: Rust wgpu/Dawn parity — render pass stays in COLOR_ATTACHMENT_OPTIMAL, barrier to PRESENT_SRC happens in ensurePresentLayout
if !view.isSwapchain {
colorFinalLayout = offscreenFinalLayout(view)
}
if hasMSAAResolve {
// With resolve, the final layout applies to the resolve target.
if resolveView.isSwapchain {
colorFinalLayout = vk.ImageLayoutPresentSrcKhr
colorFinalLayout = vk.ImageLayoutColorAttachmentOptimal // ADR-059: MSAA resolve to swapchain — barrier handles PRESENT_SRC transition
} else {
colorFinalLayout = offscreenFinalLayout(resolveView)
}
Expand Down
4 changes: 2 additions & 2 deletions hal/vulkan/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ func (d *Device) CreateRenderPipeline(desc *hal.RenderPipelineDescriptor) (hal.R
ColorLoadOp: vk.AttachmentLoadOpClear,
ColorStoreOp: vk.AttachmentStoreOpStore,
SampleCount: vk.SampleCountFlagBits(sampleCount),
ColorFinalLayout: vk.ImageLayoutPresentSrcKhr,
HasResolve: sampleCount > 1, // MSAA pipelines need resolve attachment
ColorFinalLayout: vk.ImageLayoutColorAttachmentOptimal, // ADR-059: Rust wgpu parity — pipelines use COLOR_ATTACHMENT_OPTIMAL for compatibility
HasResolve: sampleCount > 1, // MSAA pipelines need resolve attachment
}
if depthFormat != vk.FormatUndefined {
rpKey.DepthFormat = depthFormat
Expand Down
111 changes: 42 additions & 69 deletions hal/vulkan/swapchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,13 @@ type Swapchain struct {
// interference with encoder lifecycle. Created lazily on first barrier need.
barrierPool vk.CommandPool

// barrierFence synchronizes the barrier command buffer submission in
// ensurePresentLayout. We must wait for the barrier to complete on the GPU
// before resetting the command pool, otherwise the command buffer is still
// pending (VUID-vkResetCommandPool-commandPool-00040). Created lazily
// alongside barrierPool.
barrierFence vk.Fence
// ADR-059: barrierPending tracks whether a barrier command buffer from
// ensurePresentLayout is still potentially in-flight on the GPU. The pool
// reset is deferred to acquireNextImage, where the acquire fence wait
// guarantees the previous frame (including its barrier) has completed.
// This eliminates the synchronous vkWaitForFences that was previously
// required in ensurePresentLayout.
barrierPending bool

// broken is set after a synchronization, layout, or presentation failure.
// A broken swapchain cannot safely reuse its binary semaphores; callers must
Expand Down Expand Up @@ -909,17 +910,14 @@ func (sc *Swapchain) destroyResourcesAfterIdle() error {
sc.acquireFence = 0
}

if sc.barrierFence != 0 {
sc.device.cmds.DestroyFence(sc.device.handle, sc.barrierFence, nil)
sc.barrierFence = 0
}
if sc.barrierPool != 0 {
// vkDestroyCommandPool is a void Vulkan command; there is no result to
// propagate. DeviceWaitIdle above establishes the required lifetime
// guarantee before this teardown.
sc.device.cmds.DestroyCommandPool(sc.device.handle, sc.barrierPool, nil)
sc.barrierPool = 0
}
sc.barrierPending = false
sc.imageLayouts = nil
sc.currentAcquireSem = 0
sc.currentAcquireIdx = 0
Expand Down Expand Up @@ -1006,8 +1004,8 @@ func (sc *Swapchain) abandonDeviceResources() {
sc.surfaceTextures = nil
sc.imageLayouts = nil
sc.acquireFence = 0
sc.barrierFence = 0
sc.barrierPool = 0
sc.barrierPending = false
sc.currentAcquireSem = 0
sc.imageAcquired = false
sc.device = nil
Expand Down Expand Up @@ -1137,6 +1135,18 @@ func (sc *Swapchain) acquireNextImage() (*SwapchainTexture, bool, error) {
return nil, false, err
}

// ADR-059: Deferred barrier pool reset. The previous frame's barrier CB is
// guaranteed complete because acquireNextImage waits on the acquire fence,
// which requires the previous present (and therefore the barrier) to finish.
if sc.barrierPending && sc.barrierPool != 0 {
resetResult := sc.device.cmds.ResetCommandPool(sc.device.handle, sc.barrierPool, 0)
if resetResult != vk.Success {
sc.markBroken(mapVulkanResult("vkResetCommandPool (deferred barrier)", resetResult))
return nil, false, sc.failureErr
}
sc.barrierPending = false
}

// Store the current acquire index and semaphore for use in Submit.
sc.currentAcquireIdx = acquireIdx
sc.currentAcquireSem = acquireSem
Expand Down Expand Up @@ -1288,18 +1298,18 @@ func (sc *Swapchain) SetImageLayout(imageIndex uint32, layout vk.ImageLayout) {
}
}

// ensurePresentLayout checks whether the current swapchain image needs an
// explicit layout transition to PRESENT_SRC_KHR before vkQueuePresentKHR.
// ensurePresentLayout transitions the current swapchain image to
// PRESENT_SRC_KHR before vkQueuePresentKHR.
//
// In the common case (render pass directly targets the swapchain image with
// finalLayout = PRESENT_SRC_KHR), the tracked layout already matches and this
// function returns immediately — zero overhead.
// ADR-059: With render pass finalLayout = COLOR_ATTACHMENT_OPTIMAL (Rust wgpu/
// Dawn parity), this barrier fires every frame that renders to the swapchain.
// The barrier CB is submitted without a fence — the command pool reset is
// deferred to acquireNextImage, where the acquire fence wait guarantees the
// previous frame (including this barrier) has completed. This avoids a
// synchronous vkWaitForFences per frame.
//
// When the tracked layout differs (blit-only path, offscreen-only, image never
// rendered to), a one-shot command buffer is recorded with a pipeline barrier
// and submitted to the queue. This matches Chrome/Dawn's approach for the same
// edge case. The extra vkQueueSubmit is the minimum cost to guarantee spec
// compliance.
// If the tracked layout is already PRESENT_SRC_KHR (e.g., from a previous
// barrier not followed by a render pass), the function returns immediately.
//
// BUG-WGPU-VK-006: Fixes VUID-VkPresentInfoKHR-pImageIndices-01430.
func (sc *Swapchain) ensurePresentLayout(queue *Queue) error {
Expand All @@ -1317,7 +1327,7 @@ func (sc *Swapchain) ensurePresentLayout(queue *Queue) error {
return nil
}

// Need to transition. Create the barrier pool and fence lazily on first use.
// Need to transition. Create the barrier pool lazily on first use.
if sc.barrierPool == 0 {
createInfo := vk.CommandPoolCreateInfo{
SType: vk.StructureTypeCommandPoolCreateInfo,
Expand All @@ -1331,22 +1341,6 @@ func (sc *Swapchain) ensurePresentLayout(queue *Queue) error {
}
sc.device.setObjectName(vk.ObjectTypeCommandPool, uint64(pool), "PresentBarrierPool")
sc.barrierPool = pool

// Create the fence used to wait for barrier submission completion.
// VUID-vkResetCommandPool-commandPool-00040 requires all command buffers
// allocated from the pool to not be in pending state before reset.
fenceInfo := vk.FenceCreateInfo{
SType: vk.StructureTypeFenceCreateInfo,
}
var fence vk.Fence
fenceResult := sc.device.cmds.CreateFence(sc.device.handle, &fenceInfo, nil, &fence)
if fenceResult != vk.Success {
sc.device.cmds.DestroyCommandPool(sc.device.handle, pool, nil)
sc.barrierPool = 0
return fmt.Errorf("vulkan: vkCreateFence (barrier) failed: %d", fenceResult)
}
sc.device.setObjectName(vk.ObjectTypeFence, uint64(fence), "PresentBarrierFence")
sc.barrierFence = fence
}

// Allocate a one-shot command buffer from the barrier pool.
Expand Down Expand Up @@ -1431,51 +1425,30 @@ func (sc *Swapchain) ensurePresentLayout(queue *Queue) error {
return fmt.Errorf("vulkan: vkEndCommandBuffer (barrier) failed: %d", result)
}

// Submit the barrier command buffer with the barrier fence. No semaphores —
// ADR-059: Submit the barrier command buffer without a fence. No semaphores —
// this runs after the user's submit (which already waited on acquire and
// signaled present semaphores). The barrier just needs to complete before
// vkQueuePresentKHR, which is guaranteed by Vulkan's implicit ordering of
// vkQueueSubmit calls on the same queue.
//
// The fence is required so we can wait for GPU completion before resetting
// the command pool (VUID-vkResetCommandPool-commandPool-00040).
// The command pool reset is deferred to acquireNextImage, where the acquire
// fence wait guarantees the previous frame (including this barrier) has
// completed. This eliminates the synchronous vkWaitForFences that was
// previously required here — a significant win now that this barrier fires
// every frame (render pass finalLayout = COLOR_ATTACHMENT_OPTIMAL per Rust
// wgpu/Dawn parity).
submitInfo := vk.SubmitInfo{
SType: vk.StructureTypeSubmitInfo,
CommandBufferCount: 1,
PCommandBuffers: &cmdBuf,
}
result = sc.device.cmds.QueueSubmit(queue.handle, 1, &submitInfo, sc.barrierFence)
result = sc.device.cmds.QueueSubmit(queue.handle, 1, &submitInfo, vk.Fence(0))
if result != vk.Success {
return fmt.Errorf("vulkan: vkQueueSubmit (barrier) failed: %d", result)
}
sc.barrierPending = true

// Wait for the barrier submission to complete on the GPU before resetting
// the command pool. Without this wait, the command buffer is still pending
// and vkResetCommandPool violates VUID-vkResetCommandPool-commandPool-00040.
//
// This wait is synchronous but only occurs when the barrier fires (uncommon
// case: blit-only or offscreen paths where no render pass transitions the
// swapchain image to PRESENT_SRC_KHR). In the common case (render pass with
// finalLayout = PRESENT_SRC_KHR), ensurePresentLayout returns early above.
const barrierTimeout = uint64(1_000_000_000) // 1 second
waitResult := sc.device.cmds.WaitForFences(sc.device.handle, 1, &sc.barrierFence, vk.True, barrierTimeout)
if waitResult != vk.Success {
return fmt.Errorf("vulkan: vkWaitForFences (barrier) failed: %d", waitResult)
}
resetResult := sc.device.cmds.ResetFences(sc.device.handle, 1, &sc.barrierFence)
if resetResult != vk.Success {
return fmt.Errorf("vulkan: vkResetFences (barrier) failed: %d", resetResult)
}

// Reset the command pool so the buffer can be reused next frame.
// Safe now because WaitForFences guarantees the command buffer is complete.
resetPoolResult := sc.device.cmds.ResetCommandPool(sc.device.handle, sc.barrierPool, 0)
if resetPoolResult != vk.Success {
return fmt.Errorf("vulkan: vkResetCommandPool (barrier) failed: %d", resetPoolResult)
}

// Update tracked layout only after the barrier completed and its command
// buffer was safely recycled.
// Update tracked layout — the barrier transitions to PRESENT_SRC_KHR.
sc.imageLayouts[idx] = vk.ImageLayoutPresentSrcKhr

hal.Logger().Debug("vulkan: inserted PRESENT_SRC_KHR barrier",
Expand Down
Loading