From b333edb4f29d8d08cf64dc0b77ec5ee0ab1b9151 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 03:11:58 +0200 Subject: [PATCH] test(vulkan): publish readback before host mapping Record the exact sync2 COPY/TRANSFER_WRITE to HOST/HOST_READ buffer dependency over the copied readback range before ending and submitting the existing command buffer. Preserve coherent mapping and queue-idle completion. Add a portable guard that inspects the descriptor factory used by the live ReadBack path and pins copy, descriptor, barrier, end, and submit order without initializing Vulkan. Mutations performed and restored: 1. Removed the live post-copy barrier: the guard first failed with Expected copy -> descriptor -> barrier -> end -> submit, got 4260, -1, -1, -1, 4386, 4900. 2. Changed source access to TransferReadBit: the guard first failed Assert.Equal, expected Access2TransferWriteBit, actual Access2TransferReadBit. 3. Moved the correct barrier before the copy: the guard first failed with Expected copy -> descriptor -> barrier -> end -> submit, got 4657, 4260, 4525, 4594, 4783, 5297. --- .../Vk/MeshModernSharedIndexOffscreenTests.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs index b4fae7e1..3837cb6f 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/MeshModernSharedIndexOffscreenTests.cs @@ -43,6 +43,44 @@ public sealed unsafe class MeshModernSharedIndexOffscreenTests } } + [Fact] + public void ReadbackDependency_PublishesTheExactCopiedRangeBeforeEndAndSubmit() + { + const ulong byteCount = 16_384; + var readback = new Buffer(0x470u); + + BufferMemoryBarrier2 barrier = CreateHostReadBarrier(readback, byteCount); + Assert.Equal(StructureType.BufferMemoryBarrier2, barrier.SType); + Assert.Equal(PipelineStageFlags2.CopyBit, barrier.SrcStageMask); + Assert.Equal(AccessFlags2.TransferWriteBit, barrier.SrcAccessMask); + Assert.Equal(PipelineStageFlags2.HostBit, barrier.DstStageMask); + Assert.Equal(AccessFlags2.HostReadBit, barrier.DstAccessMask); + Assert.Equal(Silk.NET.Vulkan.Vk.QueueFamilyIgnored, barrier.SrcQueueFamilyIndex); + Assert.Equal(Silk.NET.Vulkan.Vk.QueueFamilyIgnored, barrier.DstQueueFamilyIndex); + Assert.Equal(readback.Handle, barrier.Buffer.Handle); + Assert.Equal(0ul, barrier.Offset); + Assert.Equal(byteCount, barrier.Size); + + string source = File.ReadAllText(Path.Combine( + RepositoryRoot(), "tests", "AcDream.App.Tests", "Rendering", "Gpu", "Vk", + "MeshModernSharedIndexOffscreenTests.cs")); + int readbackStart = source.LastIndexOf("private static byte[] ReadBack(", StringComparison.Ordinal); + int readbackEnd = source.LastIndexOf("private static int CountPixels(", StringComparison.Ordinal); + Assert.True(readbackStart >= 0 && readbackEnd > readbackStart); + string livePath = source[readbackStart..readbackEnd]; + + int copy = livePath.IndexOf("vk.CmdCopyImageToBuffer(commands, image, ImageLayout.TransferSrcOptimal, readback, 1, ©);", StringComparison.Ordinal); + int descriptor = livePath.IndexOf("BufferMemoryBarrier2 hostReadBarrier = CreateHostReadBarrier(readback, byteCount);", StringComparison.Ordinal); + int dependency = livePath.IndexOf("PBufferMemoryBarriers = &hostReadBarrier", StringComparison.Ordinal); + int publish = livePath.IndexOf("vk.CmdPipelineBarrier2(commands, &hostDependency);", StringComparison.Ordinal); + int end = livePath.IndexOf("vk.EndCommandBuffer(commands)", StringComparison.Ordinal); + int submit = livePath.IndexOf("vk.QueueSubmit2(queue, 1, &submit, default)", StringComparison.Ordinal); + Assert.True( + copy >= 0 && copy < descriptor && descriptor < dependency && dependency < publish + && publish < end && end < submit, + $"Expected copy -> descriptor -> barrier -> end -> submit, got {copy}, {descriptor}, {dependency}, {publish}, {end}, {submit}."); + } + private static byte[] Render( VulkanGpuDevice device, Silk.NET.Vulkan.Vk vk, @@ -264,6 +302,14 @@ public sealed unsafe class MeshModernSharedIndexOffscreenTests ImageExtent = new Extent3D(Extent, Extent, 1), }; vk.CmdCopyImageToBuffer(commands, image, ImageLayout.TransferSrcOptimal, readback, 1, ©); + BufferMemoryBarrier2 hostReadBarrier = CreateHostReadBarrier(readback, byteCount); + var hostDependency = new DependencyInfo + { + SType = StructureType.DependencyInfo, + BufferMemoryBarrierCount = 1, + PBufferMemoryBarriers = &hostReadBarrier, + }; + vk.CmdPipelineBarrier2(commands, &hostDependency); VulkanInterop.Check(vk.EndCommandBuffer(commands), "vkEndCommandBuffer (S5-470 readback)"); var commandInfo = new CommandBufferSubmitInfo @@ -304,6 +350,24 @@ public sealed unsafe class MeshModernSharedIndexOffscreenTests } } + private static BufferMemoryBarrier2 CreateHostReadBarrier(Buffer readback, ulong byteCount) + { + ArgumentOutOfRangeException.ThrowIfZero(byteCount); + return new BufferMemoryBarrier2 + { + SType = StructureType.BufferMemoryBarrier2, + SrcStageMask = PipelineStageFlags2.CopyBit, + SrcAccessMask = AccessFlags2.TransferWriteBit, + DstStageMask = PipelineStageFlags2.HostBit, + DstAccessMask = AccessFlags2.HostReadBit, + SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored, + Buffer = readback, + Offset = 0, + Size = byteCount, + }; + } + private static int CountPixels(ReadOnlySpan pixels, byte expected) { int count = 0;