From 5042ffd935b23209e8b9eee4d8e88631b7b8e858 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 8 Jul 2026 10:50:06 +0200 Subject: [PATCH] fix(presenter): order the vk-frame layout transition after the decode semaphore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The submit waits the Vulkan-Video frame's decode-complete timeline semaphore at FRAGMENT_SHADER, but the acquire barrier ran its layout transition (VIDEO_DECODE_DST/DPB -> SHADER_READ_ONLY) from srcStage TOP_OF_PIPE — which the wait does not cover, so no dependency chain formed and the transition could execute while the decode queue was still writing the image. The race window is real: the pump ships each frame to the presenter BEFORE its decode-complete wait (that wait is stats-only). On RADV the transition physically touches the image (metadata / decompression), so the race rendered green/yellow block corruption exactly at freshly-decoded regions — the Steam Deck 'artifacts around the moving cursor' bug (the cursor is the only damage on an idle desktop). NVIDIA treats the transition as a no-op, which is why the identical code looked clean on the dev box. srcStage is now FRAGMENT_SHADER, matching the semaphore's wait mask — the standard acquire pattern (same as swapchain acquires) that chains the transition after the decode signal. Co-Authored-By: Claude Fable 5 --- crates/pf-presenter/src/vk.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/pf-presenter/src/vk.rs b/crates/pf-presenter/src/vk.rs index 0f7c5b9a..c5042b76 100644 --- a/crates/pf-presenter/src/vk.rs +++ b/crates/pf-presenter/src/vk.rs @@ -1795,6 +1795,17 @@ fn unlock_vkframe(f: &VkVideoFrame, sync: &VkFrameSync, submitted: bool, graphic /// sharing) and transition it for sampling. `src_qf == dst_qf` (or IGNORED/CONCURRENT) /// degrades to a plain layout transition. The matching decode-side acquire happens in /// FFmpeg, keyed off the queue_family we write back after submission. +/// +/// `srcStage` is FRAGMENT_SHADER — NOT TOP_OF_PIPE — deliberately: the submit waits the +/// frame's decode-complete timeline semaphore with `wait_dst_stage_mask = +/// FRAGMENT_SHADER`, and a semaphore wait only orders operations whose first sync scope +/// INTERSECTS that mask (the dependency-chain rule). With TOP_OF_PIPE the barrier's +/// layout transition (VIDEO_DECODE_DST/DPB → SHADER_READ_ONLY) formed no chain with the +/// wait and could execute while the decode queue was still writing the image. On RADV +/// that transition physically touches the image (metadata/decompression), so the race +/// showed as green/yellow block corruption exactly at freshly-decoded (damaged) regions +/// — the Steam Deck cursor-trail artifact. NVIDIA treats the transition as a no-op, +/// which is why the same code looked clean there. fn vkframe_acquire_barrier( device: &ash::Device, cmd: vk::CommandBuffer, @@ -1820,7 +1831,7 @@ fn vkframe_acquire_barrier( unsafe { device.cmd_pipeline_barrier( cmd, - vk::PipelineStageFlags::TOP_OF_PIPE, + vk::PipelineStageFlags::FRAGMENT_SHADER, vk::PipelineStageFlags::FRAGMENT_SHADER, vk::DependencyFlags::empty(), &[],