fix: land reviewed GPU synchronization repairs with verification ledger

This commit is contained in:
Erik 2026-09-05 12:08:22 +02:00
parent 34d050fabc
commit 73de7403c3
8 changed files with 881 additions and 99 deletions

View file

@ -1055,6 +1055,14 @@ withdraw first, then owner leases and caches, then device backing stores. This k
drivers from reading freed memory without adding a portal-specific purge or a
visual-distance reduction.
Native Vulkan dependencies remain scoped to their real producers and consumers.
The acquired-image semaphore wait and first swapchain transition both use the
color-output stage; fixed-function depth/stencil resolve destinations use the
color-output writer masks rather than the ordinary depth-test masks; and each
device-buffer migration publishes prior transfer writes to its exact source
range before the migration read. Host-staged uploads remain covered by the one
batched trailing draw-visibility barrier rather than gaining per-copy barriers.
---
## Per-Frame Update Order (current runtime)

View file

@ -721,7 +721,8 @@ Update immediately when a slice changes state. Chat is not the ledger.
witness returned; lead independently passed Release0W0E/314 portableGPU and
repeated all four discriminating mutations with byte-identical restoration.
477-lead-verification.md records proof; API/behavior review1 PASS/no findings,
production/lifetime review2 in progress; integration/native validation owed.
production/lifetime review2 PASS/no findings. Scratch4f4ea8a50 integrated with
this ledger, unchanged five reviewed blobs; fresh build/native validation next.
#476 separately dispatched to James at6347e0f82; #480 returned eight files,
agent92/92 tests, pending lead verification/reviews. No new client run. Lead verified
#480's retail/paired-binary child publication and contracted the separate

View file

@ -57,5 +57,9 @@ F1 command-witness gap is closed by this proof; production unchanged from
round0. Independent API/behavior lens1 by Wegener PASS, no actionable finding:
actual native producer/consumer bindings, narrow masks, exact ranges, source
hashes and four lead mutation outcomes checked. Rawls now performs sequential
production/lifetime lens2. Integration and native validation remain owed.
G4 remains FAIL. No intentional deviation or new register row.
production/lifetime lens2 also PASS, no actionable findings: real copy
consumer/retirement, queue clear, frame bindings, exact five-file scope and
hashes checked. Two independent reviews total, one prior test-only F1.
Lead committed exact scratch4f4ea8a50f563274ce0afc342cff070d3458c452 and
integrated its unchanged five blobs with this ledger. Native validation and
fresh integrated build remain next. G4 FAIL; no new intentional deviation.

View file

@ -4722,7 +4722,8 @@ Lead independently repeated all four mutations, observed their intended FAILs,
restored raw hashes and ran Release0W0E plus314/314 portableGPU tests. The
lead's original F1 evidence gap is closed;477-lead-verification.md records
exact results. Wegener API/behavior review1 PASS, no findings; Rawls performs
production/lifetime review2. Native graphical validation owed. No integration yet.
production/lifetime review2 also PASS/no findings. Reviewed scratch4f4ea8a50
integrates unchanged five blobs with this ledger; native validation owed.
#476 dispatched to James in fresh codex/s5-476-openai-impl at6347e0f8269e201340b0c7a6b826ad4cfc21f43e,
using476-completed-frame-capture-contract.md. #477 scratch is frozen; root

View file

@ -1047,46 +1047,47 @@ internal sealed unsafe partial class VulkanGpuDevice
bool first = !_backbufferRenderingReady;
_backbufferRenderingReady = true;
var barrier = new ImageMemoryBarrier2
{
SType = StructureType.ImageMemoryBarrier2,
SrcStageMask = first
? PipelineStageFlags2.TopOfPipeBit
: PipelineStageFlags2.ColorAttachmentOutputBit,
SrcAccessMask = first
? AccessFlags2.None
: AccessFlags2.ColorAttachmentWriteBit,
DstStageMask = PipelineStageFlags2.ColorAttachmentOutputBit,
DstAccessMask = first
? AccessFlags2.ColorAttachmentWriteBit
: AccessFlags2.ColorAttachmentWriteBit | AccessFlags2.ColorAttachmentReadBit,
// Undefined for the acquire — the contents are genuinely undefined
// and saying so lets the driver skip a decompress. A later pass in
// the same frame must NOT say Undefined: that would license
// discarding everything drawn so far.
OldLayout = first ? ImageLayout.Undefined : ImageLayout.ColorAttachmentOptimal,
NewLayout = ImageLayout.ColorAttachmentOptimal,
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = new ImageSubresourceRange
{
AspectMask = ImageAspectFlags.ColorBit,
BaseMipLevel = 0,
LevelCount = 1,
BaseArrayLayer = 0,
LayerCount = 1,
},
};
var dependency = new DependencyInfo
{
SType = StructureType.DependencyInfo,
ImageMemoryBarrierCount = 1,
PImageMemoryBarriers = &barrier,
};
_vk.CmdPipelineBarrier2(commands, &dependency);
ImageMemoryBarrier2 barrier = CreateBackbufferRenderingBarrier(image, first);
SubmitImageBarrier(commands, barrier);
}
internal static ImageMemoryBarrier2 CreateBackbufferRenderingBarrier(
Image image,
bool first) => new()
{
SType = StructureType.ImageMemoryBarrier2,
// The first image use is ordered by the acquire semaphore, whose wait
// is scoped to this same stage. TopOfPipe here would run before that
// wait and race the presentation engine's ownership/layout use.
SrcStageMask = first
? AcquiredImageWaitStage
: PipelineStageFlags2.ColorAttachmentOutputBit,
SrcAccessMask = first
? AccessFlags2.None
: AccessFlags2.ColorAttachmentWriteBit,
DstStageMask = PipelineStageFlags2.ColorAttachmentOutputBit,
DstAccessMask = first
? AccessFlags2.ColorAttachmentWriteBit
: AccessFlags2.ColorAttachmentWriteBit | AccessFlags2.ColorAttachmentReadBit,
// Undefined for the acquire — the contents are genuinely undefined
// and saying so lets the driver skip a decompress. A later pass in
// the same frame must NOT say Undefined: that would license
// discarding everything drawn so far.
OldLayout = first ? ImageLayout.Undefined : ImageLayout.ColorAttachmentOptimal,
NewLayout = ImageLayout.ColorAttachmentOptimal,
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = new ImageSubresourceRange
{
AspectMask = ImageAspectFlags.ColorBit,
BaseMipLevel = 0,
LevelCount = 1,
BaseArrayLayer = 0,
LayerCount = 1,
},
};
/// <summary>
/// Campaign V slice V6g: moves the multisampled colour scratch into
/// <c>COLOR_ATTACHMENT_OPTIMAL</c> before the pass that names it there.
@ -1173,31 +1174,19 @@ internal sealed unsafe partial class VulkanGpuDevice
if (target.DepthAttachment is { } depth)
{
TransitionImage(
commands,
SubmitImageBarrier(commands, CreateRenderTargetDepthEntryBarrier(
depth.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
depth.CurrentLayout,
ImageLayout.DepthStencilAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit);
fixedFunctionResolve: false));
depth.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
}
if (target.DepthResolve is { } depthResolve)
{
TransitionImage(
commands,
SubmitImageBarrier(commands, CreateRenderTargetDepthEntryBarrier(
depthResolve.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
depthResolve.CurrentLayout,
ImageLayout.DepthStencilAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit);
fixedFunctionResolve: true));
depthResolve.MarkLayout(ImageLayout.DepthStencilAttachmentOptimal);
}
}
@ -1268,20 +1257,58 @@ internal sealed unsafe partial class VulkanGpuDevice
if (depthStored && target.Description.SampleableDepth && target.DepthResult is { } depth)
{
TransitionImage(
commands,
SubmitImageBarrier(commands, CreateRenderTargetDepthSamplingBarrier(
depth.Image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
depth.CurrentLayout,
ImageLayout.DepthStencilReadOnlyOptimal,
PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit,
AccessFlags2.DepthStencilAttachmentWriteBit,
PipelineStageFlags2.FragmentShaderBit,
AccessFlags2.ShaderReadBit);
fixedFunctionResolve: target.DepthResolve is not null));
depth.MarkLayout(ImageLayout.DepthStencilReadOnlyOptimal);
}
}
internal static ImageMemoryBarrier2 CreateRenderTargetDepthEntryBarrier(
Image image,
ImageLayout oldLayout,
bool fixedFunctionResolve)
{
PipelineStageFlags2 writerStage = fixedFunctionResolve
? PipelineStageFlags2.ColorAttachmentOutputBit
: PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit;
AccessFlags2 writerAccess = fixedFunctionResolve
? AccessFlags2.ColorAttachmentWriteBit
: AccessFlags2.DepthStencilAttachmentWriteBit;
return CreateImageBarrier(
image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
oldLayout,
ImageLayout.DepthStencilAttachmentOptimal,
PipelineStageFlags2.AllCommandsBit,
AccessFlags2.None,
writerStage,
writerAccess);
}
internal static ImageMemoryBarrier2 CreateRenderTargetDepthSamplingBarrier(
Image image,
ImageLayout oldLayout,
bool fixedFunctionResolve)
{
PipelineStageFlags2 writerStage = fixedFunctionResolve
? PipelineStageFlags2.ColorAttachmentOutputBit
: PipelineStageFlags2.EarlyFragmentTestsBit | PipelineStageFlags2.LateFragmentTestsBit;
AccessFlags2 writerAccess = fixedFunctionResolve
? AccessFlags2.ColorAttachmentWriteBit
: AccessFlags2.DepthStencilAttachmentWriteBit;
return CreateImageBarrier(
image,
ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit,
oldLayout,
ImageLayout.DepthStencilReadOnlyOptimal,
writerStage,
writerAccess,
PipelineStageFlags2.FragmentShaderBit,
AccessFlags2.ShaderReadBit);
}
private void TransitionDirectionalDepthForRendering(
CommandBuffer commands,
VulkanDirectionalDepthTarget target,
@ -1347,27 +1374,53 @@ internal sealed unsafe partial class VulkanGpuDevice
uint baseArrayLayer = 0,
uint layerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers)
{
var barrier = new ImageMemoryBarrier2
SubmitImageBarrier(commands, CreateImageBarrier(
image,
aspect,
oldLayout,
newLayout,
sourceStage,
sourceAccess,
destinationStage,
destinationAccess,
baseArrayLayer,
layerCount));
}
private static ImageMemoryBarrier2 CreateImageBarrier(
Image image,
ImageAspectFlags aspect,
ImageLayout oldLayout,
ImageLayout newLayout,
PipelineStageFlags2 sourceStage,
AccessFlags2 sourceAccess,
PipelineStageFlags2 destinationStage,
AccessFlags2 destinationAccess,
uint baseArrayLayer = 0,
uint layerCount = Silk.NET.Vulkan.Vk.RemainingArrayLayers) => new()
{
SType = StructureType.ImageMemoryBarrier2,
SrcStageMask = sourceStage,
SrcAccessMask = sourceAccess,
DstStageMask = destinationStage,
DstAccessMask = destinationAccess,
OldLayout = oldLayout,
NewLayout = newLayout,
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = new ImageSubresourceRange
{
SType = StructureType.ImageMemoryBarrier2,
SrcStageMask = sourceStage,
SrcAccessMask = sourceAccess,
DstStageMask = destinationStage,
DstAccessMask = destinationAccess,
OldLayout = oldLayout,
NewLayout = newLayout,
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = new ImageSubresourceRange
{
AspectMask = aspect,
BaseMipLevel = 0,
LevelCount = Silk.NET.Vulkan.Vk.RemainingMipLevels,
BaseArrayLayer = baseArrayLayer,
LayerCount = layerCount,
},
};
AspectMask = aspect,
BaseMipLevel = 0,
LevelCount = Silk.NET.Vulkan.Vk.RemainingMipLevels,
BaseArrayLayer = baseArrayLayer,
LayerCount = layerCount,
},
};
private void SubmitImageBarrier(CommandBuffer commands, ImageMemoryBarrier2 barrier)
{
var dependency = new DependencyInfo
{
SType = StructureType.DependencyInfo,

View file

@ -72,6 +72,14 @@ internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice, IGpuPipelineF
/// <summary>Per-flight-slot ring capacity, matching the GL backend's 16 MiB.</summary>
internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024;
/// <summary>
/// The acquired-image semaphore and the first swapchain-image transition
/// form one execution dependency. Keep the stage in one place so the
/// submit cannot drift from the barrier that consumes the acquired image.
/// </summary>
internal const PipelineStageFlags2 AcquiredImageWaitStage =
PipelineStageFlags2.ColorAttachmentOutputBit;
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly PhysicalDevice _physicalDevice;
private readonly Device _device;
@ -447,12 +455,7 @@ internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice, IGpuPipelineF
SType = StructureType.CommandBufferSubmitInfo,
CommandBuffer = commands,
};
var waitSemaphore = new SemaphoreSubmitInfo
{
SType = StructureType.SemaphoreSubmitInfo,
Semaphore = _imageAcquired[slot],
StageMask = PipelineStageFlags2.ColorAttachmentOutputBit,
};
SemaphoreSubmitInfo waitSemaphore = CreateAcquiredImageWait(_imageAcquired[slot]);
SemaphoreSubmitInfo* signals = stackalloc SemaphoreSubmitInfo[2];
int signalCount = 0;
if (_acquiredImageIndex is { } presented && _backbuffer is not null)
@ -497,6 +500,13 @@ internal sealed unsafe partial class VulkanGpuDevice : IGpuDevice, IGpuPipelineF
}
}
internal static SemaphoreSubmitInfo CreateAcquiredImageWait(Semaphore semaphore) => new()
{
SType = StructureType.SemaphoreSubmitInfo,
Semaphore = semaphore,
StageMask = AcquiredImageWaitStage,
};
/// <summary>False after a present that reported the swapchain should be rebuilt.</summary>
internal bool PresentSucceeded { get; private set; } = true;

View file

@ -16,10 +16,12 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// direct analogue of the GL backend's "flush immediately before every draw"
/// discipline, moved to the coarser granularity Vulkan actually needs.</para>
///
/// <para><b>One batched barrier, not one per copy.</b> The drain emits a single
/// buffer memory barrier covering every copy it recorded, moving the whole batch
/// from transfer writes to the vertex/index/indirect/shader reads that follow.
/// Plan §4.8 budgets four to six barriers per frame; this is one of them.</para>
/// <para><b>One batched consumer barrier, not one per staging copy.</b> The drain
/// emits a single buffer memory barrier covering every copy it recorded, moving
/// the whole batch from transfer writes to the vertex/index/indirect/shader reads
/// that follow. A device-buffer migration additionally needs one narrow source-
/// range transfer-write to transfer-read dependency before its copy; ordinary
/// host-staged writes do not. Plan §4.8 budgets the shared trailing barrier.</para>
///
/// <para><b>Staging exhaustion is not an error.</b> When the ring cannot serve a
/// request — the payload is larger than the whole ring, or unretired frames hold
@ -50,7 +52,19 @@ internal sealed unsafe class VulkanUploadQueue : IDisposable
private bool _disposed;
private readonly record struct BufferCopy2(Buffer Source, Buffer Destination, ulong SourceOffset, ulong DestinationOffset, ulong SizeBytes);
internal enum BufferCopyKind
{
HostStaging,
DeviceMigration,
}
private readonly record struct BufferCopy2(
Buffer Source,
Buffer Destination,
ulong SourceOffset,
ulong DestinationOffset,
ulong SizeBytes,
BufferCopyKind Kind);
private readonly record struct ImageCopy2(
Buffer Source,
@ -130,7 +144,8 @@ internal sealed unsafe class VulkanUploadQueue : IDisposable
destination,
sourceOffset,
destinationOffsetBytes,
(ulong)data.Length));
(ulong)data.Length,
BufferCopyKind.HostStaging));
}
/// <summary>Stages <paramref name="data"/> and queues a copy into one mip level of one array layer.</summary>
@ -207,7 +222,8 @@ internal sealed unsafe class VulkanUploadQueue : IDisposable
destination,
sourceOffsetBytes,
destinationOffsetBytes,
byteCount));
byteCount,
BufferCopyKind.DeviceMigration));
}
/// <summary>
@ -225,6 +241,21 @@ internal sealed unsafe class VulkanUploadQueue : IDisposable
foreach (BufferCopy2 copy in _bufferCopies)
{
if (RequiresDeviceMigrationReadBarrier(copy.Kind))
{
BufferMemoryBarrier2 barrier = CreateDeviceMigrationReadBarrier(
copy.Source,
copy.SourceOffset,
copy.SizeBytes);
var dependency = new DependencyInfo
{
SType = StructureType.DependencyInfo,
BufferMemoryBarrierCount = 1,
PBufferMemoryBarriers = &barrier,
};
_vk.CmdPipelineBarrier2(commands, &dependency);
}
var region = new BufferCopy
{
SrcOffset = copy.SourceOffset,
@ -301,6 +332,38 @@ internal sealed unsafe class VulkanUploadQueue : IDisposable
return true;
}
/// <summary>
/// Makes earlier transfer writes to a device-buffer migration's exact
/// source range visible to its transfer read. This is emitted for each
/// migration—not for host-staged uploads—so same-drain A-&gt;B-&gt;C chains
/// and producers from earlier submissions share the same narrow rule.
/// </summary>
internal static BufferMemoryBarrier2 CreateDeviceMigrationReadBarrier(
Buffer source,
ulong sourceOffsetBytes,
ulong byteCount)
{
if (byteCount == 0)
throw new ArgumentOutOfRangeException(nameof(byteCount));
return new BufferMemoryBarrier2
{
SType = StructureType.BufferMemoryBarrier2,
SrcStageMask = PipelineStageFlags2.AllTransferBit,
SrcAccessMask = AccessFlags2.TransferWriteBit,
DstStageMask = PipelineStageFlags2.AllTransferBit,
DstAccessMask = AccessFlags2.TransferReadBit,
SrcQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Silk.NET.Vulkan.Vk.QueueFamilyIgnored,
Buffer = source,
Offset = sourceOffsetBytes,
Size = byteCount,
};
}
internal static bool RequiresDeviceMigrationReadBarrier(BufferCopyKind kind) =>
kind == BufferCopyKind.DeviceMigration;
private static ImageSubresourceRange WholeColorImage => new()
{
AspectMask = ImageAspectFlags.ColorBit,

View file

@ -0,0 +1,642 @@
using System.Collections;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu.Vk;
using Silk.NET.Core.Contexts;
using Silk.NET.Vulkan;
using VkBuffer = Silk.NET.Vulkan.Buffer;
using VkImage = Silk.NET.Vulkan.Image;
using VkSemaphore = Silk.NET.Vulkan.Semaphore;
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
public sealed class VulkanSynchronizationDependencyTests
{
private const PipelineStageFlags2 DepthStages =
PipelineStageFlags2.EarlyFragmentTestsBit |
PipelineStageFlags2.LateFragmentTestsBit;
[Fact]
public void AcquiredImage_FirstTransitionChainsToTheActualSemaphoreWait()
{
var image = new VkImage(0x4771u);
var semaphore = new VkSemaphore(0x4772u);
SemaphoreSubmitInfo wait = VulkanGpuDevice.CreateAcquiredImageWait(semaphore);
ImageMemoryBarrier2 barrier =
VulkanGpuDevice.CreateBackbufferRenderingBarrier(image, first: true);
Assert.Equal(StructureType.SemaphoreSubmitInfo, wait.SType);
Assert.Equal(semaphore, wait.Semaphore);
Assert.Equal(VulkanGpuDevice.AcquiredImageWaitStage, wait.StageMask);
Assert.Equal(wait.StageMask, barrier.SrcStageMask);
Assert.Equal(AccessFlags2.None, barrier.SrcAccessMask);
Assert.Equal(PipelineStageFlags2.ColorAttachmentOutputBit, barrier.DstStageMask);
Assert.Equal(AccessFlags2.ColorAttachmentWriteBit, barrier.DstAccessMask);
Assert.Equal(ImageLayout.Undefined, barrier.OldLayout);
Assert.Equal(ImageLayout.ColorAttachmentOptimal, barrier.NewLayout);
Assert.Equal(image, barrier.Image);
AssertColorImage(barrier.SubresourceRange);
}
[Fact]
public void AcquiredImage_SubsequentPassPreservesPriorColorWrites()
{
var image = new VkImage(0x4773u);
ImageMemoryBarrier2 barrier =
VulkanGpuDevice.CreateBackbufferRenderingBarrier(image, first: false);
Assert.Equal(PipelineStageFlags2.ColorAttachmentOutputBit, barrier.SrcStageMask);
Assert.Equal(AccessFlags2.ColorAttachmentWriteBit, barrier.SrcAccessMask);
Assert.Equal(PipelineStageFlags2.ColorAttachmentOutputBit, barrier.DstStageMask);
Assert.Equal(
AccessFlags2.ColorAttachmentWriteBit | AccessFlags2.ColorAttachmentReadBit,
barrier.DstAccessMask);
Assert.Equal(ImageLayout.ColorAttachmentOptimal, barrier.OldLayout);
Assert.Equal(ImageLayout.ColorAttachmentOptimal, barrier.NewLayout);
Assert.Equal(image, barrier.Image);
AssertColorImage(barrier.SubresourceRange);
}
[Fact]
public void BackbufferBarrier_IsBoundToOrdinaryAndFilmicProductionPasses()
{
string resources = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanGpuDevice.Resources.cs");
string frame = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanGpuFrame.cs");
string ordinary = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanCompositionFramePhases.cs");
string filmic = Source("src", "AcDream.App", "Rendering", "Packs",
"AtmosphericPostProcessGraph.cs");
Assert.Contains(
"ImageMemoryBarrier2 barrier = CreateBackbufferRenderingBarrier(image, first);",
resources,
StringComparison.Ordinal);
string device = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanGpuDevice.cs");
Assert.Contains(
"SemaphoreSubmitInfo waitSemaphore = CreateAcquiredImageWait(_imageAcquired[slot]);",
device,
StringComparison.Ordinal);
Assert.Contains("_device.BeginPass(this, description)", frame, StringComparison.Ordinal);
Assert.Contains("Name = \"vk-world\"", ordinary, StringComparison.Ordinal);
Assert.Contains("Target: null", ordinary, StringComparison.Ordinal);
Assert.Contains("\"atmospheric-filmic\"", filmic, StringComparison.Ordinal);
Assert.Contains("target: null", filmic, StringComparison.Ordinal);
}
[Fact]
public void DepthTransitions_DistinguishSingleSampleSampleableDepth()
{
var image = new VkImage(0x4774u);
ImageMemoryBarrier2 entry = VulkanGpuDevice.CreateRenderTargetDepthEntryBarrier(
image,
ImageLayout.Undefined,
fixedFunctionResolve: false);
ImageMemoryBarrier2 exit = VulkanGpuDevice.CreateRenderTargetDepthSamplingBarrier(
image,
ImageLayout.DepthStencilAttachmentOptimal,
fixedFunctionResolve: false);
AssertDepthEntry(entry, image, DepthStages, AccessFlags2.DepthStencilAttachmentWriteBit);
AssertDepthExit(exit, image, DepthStages, AccessFlags2.DepthStencilAttachmentWriteBit);
}
[Fact]
public void DepthTransitions_MultisampleNonSampleableDepthKeepsOrdinaryWriterMasks()
{
var image = new VkImage(0x4775u);
ImageMemoryBarrier2 entry = VulkanGpuDevice.CreateRenderTargetDepthEntryBarrier(
image,
ImageLayout.Undefined,
fixedFunctionResolve: false);
AssertDepthEntry(entry, image, DepthStages, AccessFlags2.DepthStencilAttachmentWriteBit);
}
[Fact]
public void DepthTransitions_SampleableMsaaResolveUsesColorWriterAtEntryAndExit()
{
var image = new VkImage(0x4776u);
ImageMemoryBarrier2 entry = VulkanGpuDevice.CreateRenderTargetDepthEntryBarrier(
image,
ImageLayout.Undefined,
fixedFunctionResolve: true);
ImageMemoryBarrier2 exit = VulkanGpuDevice.CreateRenderTargetDepthSamplingBarrier(
image,
ImageLayout.DepthStencilAttachmentOptimal,
fixedFunctionResolve: true);
AssertDepthEntry(
entry,
image,
PipelineStageFlags2.ColorAttachmentOutputBit,
AccessFlags2.ColorAttachmentWriteBit);
AssertDepthExit(
exit,
image,
PipelineStageFlags2.ColorAttachmentOutputBit,
AccessFlags2.ColorAttachmentWriteBit);
}
[Fact]
public void DepthBarrierFactories_AreBoundToTheActualAttachmentAndResolveBranches()
{
string source = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanGpuDevice.Resources.cs");
Assert.Contains(
"depth.CurrentLayout,\n fixedFunctionResolve: false));",
Normalize(source),
StringComparison.Ordinal);
Assert.Contains(
"depthResolve.CurrentLayout,\n fixedFunctionResolve: true));",
Normalize(source),
StringComparison.Ordinal);
Assert.Contains(
"fixedFunctionResolve: target.DepthResolve is not null));",
Normalize(source),
StringComparison.Ordinal);
Assert.Contains(
"depthAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;",
source,
StringComparison.Ordinal);
Assert.Contains(
"stencilAttachment.ResolveMode = ResolveModeFlags.SampleZeroBit;",
source,
StringComparison.Ordinal);
string target = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanGpuRenderTarget.cs");
Assert.Contains(
"int retainedDepthSamples =\n description.SampleableDepth ? 1 : description.SampleCount;",
Normalize(target),
StringComparison.Ordinal);
Assert.Contains(
"Description.SampleableDepth && _multisampleDepth is not null ? _depth : null",
target,
StringComparison.Ordinal);
}
[Fact]
public void DeviceMigrationBarrier_CoversTheExactSourceRange()
{
var source = new VkBuffer(0x4777u);
BufferMemoryBarrier2 barrier = VulkanUploadQueue.CreateDeviceMigrationReadBarrier(
source,
sourceOffsetBytes: 4096,
byteCount: 8192);
Assert.Equal(StructureType.BufferMemoryBarrier2, barrier.SType);
Assert.Equal(PipelineStageFlags2.AllTransferBit, barrier.SrcStageMask);
Assert.Equal(AccessFlags2.TransferWriteBit, barrier.SrcAccessMask);
Assert.Equal(PipelineStageFlags2.AllTransferBit, barrier.DstStageMask);
Assert.Equal(AccessFlags2.TransferReadBit, barrier.DstAccessMask);
Assert.Equal(source, barrier.Buffer);
Assert.Equal(4096ul, barrier.Offset);
Assert.Equal(8192ul, barrier.Size);
Assert.Equal(Silk.NET.Vulkan.Vk.QueueFamilyIgnored, barrier.SrcQueueFamilyIndex);
Assert.Equal(Silk.NET.Vulkan.Vk.QueueFamilyIgnored, barrier.DstQueueFamilyIndex);
}
[Fact]
public void DeviceMigrationBarrier_RejectsAnEmptyRange()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
VulkanUploadQueue.CreateDeviceMigrationReadBarrier(default, 0, 0));
}
[Fact]
public void UploadQueue_ActualNativeRecord_StagesThenMigratesWithExactDependencyAndOneConsumerBarrier()
{
using var native = new RecordingNativeContext();
var queue = CreateCommandWitnessQueue(native);
var commands = new CommandBuffer((nint)0xC04771u);
var staging = new VkBuffer(0xA04771u);
var a = new VkBuffer(0xA04772u);
var b = new VkBuffer(0xA04773u);
EnqueueHostStagingCopy(queue, staging, a, sourceOffset: 16, destinationOffset: 32, size: 64);
queue.EnqueueBufferCopy(a, b, sourceOffsetBytes: 48, destinationOffsetBytes: 96, byteCount: 128);
Assert.True(queue.Record(commands));
Assert.Collection(
native.Commands,
command => AssertCopy(command, commands, staging, a, 16, 32, 64),
command => AssertMigrationBarrier(command, commands, a, 48, 128),
command => AssertCopy(command, commands, a, b, 48, 96, 128),
command => AssertTrailingConsumerBarrier(command, commands));
int emitted = native.Commands.Count;
Assert.False(queue.Record(commands));
Assert.Equal(emitted, native.Commands.Count);
}
[Fact]
public void UploadQueue_ActualNativeRecord_EmitsEachMigrationDependencyInAnAToBToCChain()
{
using var native = new RecordingNativeContext();
var queue = CreateCommandWitnessQueue(native);
var commands = new CommandBuffer((nint)0xC04772u);
var a = new VkBuffer(0xB04771u);
var b = new VkBuffer(0xB04772u);
var c = new VkBuffer(0xB04773u);
queue.EnqueueBufferCopy(a, b, sourceOffsetBytes: 64, destinationOffsetBytes: 80, byteCount: 144);
queue.EnqueueBufferCopy(b, c, sourceOffsetBytes: 96, destinationOffsetBytes: 112, byteCount: 288);
Assert.True(queue.Record(commands));
Assert.Collection(
native.Commands,
command => AssertMigrationBarrier(command, commands, a, 64, 144),
command => AssertCopy(command, commands, a, b, 64, 80, 144),
command => AssertMigrationBarrier(command, commands, b, 96, 288),
command => AssertCopy(command, commands, b, c, 96, 112, 288),
command => AssertTrailingConsumerBarrier(command, commands));
}
[Fact]
public void UploadQueue_ActualNativeRecord_EmitsMigrationDependencyForAnEarlierDrainProducer()
{
using var native = new RecordingNativeContext();
var queue = CreateCommandWitnessQueue(native);
var commands = new CommandBuffer((nint)0xC04773u);
var staging = new VkBuffer(0xD04771u);
var a = new VkBuffer(0xD04772u);
var b = new VkBuffer(0xD04773u);
EnqueueHostStagingCopy(queue, staging, a, sourceOffset: 256, destinationOffset: 512, size: 1024);
Assert.True(queue.Record(commands));
Assert.Collection(
native.Commands,
command => AssertCopy(command, commands, staging, a, 256, 512, 1024),
command => AssertTrailingConsumerBarrier(command, commands));
queue.EnqueueBufferCopy(a, b, sourceOffsetBytes: 640, destinationOffsetBytes: 768, byteCount: 896);
Assert.True(queue.Record(commands));
Assert.Collection(
native.Commands,
command => AssertCopy(command, commands, staging, a, 256, 512, 1024),
command => AssertTrailingConsumerBarrier(command, commands),
command => AssertMigrationBarrier(command, commands, a, 640, 896),
command => AssertCopy(command, commands, a, b, 640, 768, 896),
command => AssertTrailingConsumerBarrier(command, commands));
}
[Fact]
public void UploadQueue_ActualNativeRecordBodyRemainsBoundToRecord()
{
string source = Source("src", "AcDream.App", "Rendering", "Gpu", "Vk",
"VulkanUploadQueue.cs");
int loopStart = source.IndexOf(
"foreach (BufferCopy2 copy in _bufferCopies)",
StringComparison.Ordinal);
int loopEnd = source.IndexOf(
"foreach (ImageCopy2 copy in _imageCopies)",
loopStart,
StringComparison.Ordinal);
Assert.True(loopStart >= 0 && loopEnd > loopStart);
string loop = source[loopStart..loopEnd];
int predicate = loop.IndexOf(
"RequiresDeviceMigrationReadBarrier(copy.Kind)",
StringComparison.Ordinal);
int descriptor = loop.IndexOf(
"CreateDeviceMigrationReadBarrier(",
StringComparison.Ordinal);
int dependency = loop.IndexOf(
"_vk.CmdPipelineBarrier2(commands, &dependency);",
StringComparison.Ordinal);
int copy = loop.IndexOf(
"_vk.CmdCopyBuffer(commands, copy.Source, copy.Destination, 1, &region);",
StringComparison.Ordinal);
Assert.True(predicate >= 0 && descriptor > predicate && dependency > descriptor && copy > dependency);
Assert.Contains(
"BufferCopyKind.HostStaging",
MethodBody(source, "internal void StageBufferWrite(", "internal void StageImageWrite("),
StringComparison.Ordinal);
Assert.Contains(
"BufferCopyKind.DeviceMigration",
MethodBody(source, "internal void EnqueueBufferCopy(", "internal bool Record("),
StringComparison.Ordinal);
// Independent staging uploads still share the original single
// transfer->draw visibility barrier after all buffer copies.
Assert.Contains(
"SrcAccessMask = AccessFlags2.TransferWriteBit",
source[loopEnd..],
StringComparison.Ordinal);
Assert.Contains(
"DstStageMask = PipelineStageFlags2.VertexInputBit",
source[loopEnd..],
StringComparison.Ordinal);
Assert.Contains(
"DstAccessMask = AccessFlags2.VertexAttributeReadBit",
source[loopEnd..],
StringComparison.Ordinal);
}
private static VulkanUploadQueue CreateCommandWitnessQueue(RecordingNativeContext native)
{
var queue = (VulkanUploadQueue)RuntimeHelpers.GetUninitializedObject(typeof(VulkanUploadQueue));
SetField(queue, "_vk", new Silk.NET.Vulkan.Vk(native));
InitializeCollectionField(queue, "_bufferCopies");
InitializeCollectionField(queue, "_imageCopies");
InitializeCollectionField(queue, "_mipBlits");
InitializeCollectionField(queue, "_imageEntryLayouts");
return queue;
}
private static void EnqueueHostStagingCopy(
VulkanUploadQueue queue,
VkBuffer source,
VkBuffer destination,
ulong sourceOffset,
ulong destinationOffset,
ulong size)
{
FieldInfo field = Field("_bufferCopies");
var copies = Assert.IsAssignableFrom<IList>(field.GetValue(queue));
Type copyType = field.FieldType.GenericTypeArguments.Single();
object copy = Activator.CreateInstance(
copyType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
source,
destination,
sourceOffset,
destinationOffset,
size,
VulkanUploadQueue.BufferCopyKind.HostStaging,
],
culture: null) ?? throw new InvalidOperationException("Could not construct pending staging copy.");
copies.Add(copy);
}
private static void InitializeCollectionField(VulkanUploadQueue queue, string name)
{
FieldInfo field = Field(name);
SetField(queue, name, Activator.CreateInstance(field.FieldType)!);
}
private static void SetField(VulkanUploadQueue queue, string name, object value) =>
Field(name).SetValue(queue, value);
private static FieldInfo Field(string name) =>
typeof(VulkanUploadQueue).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException($"Missing VulkanUploadQueue field {name}.");
private static void AssertCopy(
RecordedNativeCommand command,
CommandBuffer commands,
VkBuffer source,
VkBuffer destination,
ulong sourceOffset,
ulong destinationOffset,
ulong size)
{
Assert.Equal(RecordedNativeCommandKind.CopyBuffer, command.Kind);
Assert.Equal(commands, command.Commands);
Assert.Equal(source, command.Source);
Assert.Equal(destination, command.Destination);
Assert.Equal(1u, command.RegionCount);
Assert.Equal(sourceOffset, command.Copy.SrcOffset);
Assert.Equal(destinationOffset, command.Copy.DstOffset);
Assert.Equal(size, command.Copy.Size);
}
private static void AssertMigrationBarrier(
RecordedNativeCommand command,
CommandBuffer commands,
VkBuffer source,
ulong sourceOffset,
ulong size)
{
Assert.Equal(RecordedNativeCommandKind.BufferBarrier, command.Kind);
Assert.Equal(commands, command.Commands);
BufferMemoryBarrier2 barrier = command.BufferBarrier;
Assert.Equal(StructureType.BufferMemoryBarrier2, barrier.SType);
Assert.Equal(PipelineStageFlags2.AllTransferBit, barrier.SrcStageMask);
Assert.Equal(AccessFlags2.TransferWriteBit, barrier.SrcAccessMask);
Assert.Equal(PipelineStageFlags2.AllTransferBit, barrier.DstStageMask);
Assert.Equal(AccessFlags2.TransferReadBit, barrier.DstAccessMask);
Assert.Equal(Silk.NET.Vulkan.Vk.QueueFamilyIgnored, barrier.SrcQueueFamilyIndex);
Assert.Equal(Silk.NET.Vulkan.Vk.QueueFamilyIgnored, barrier.DstQueueFamilyIndex);
Assert.Equal(source, barrier.Buffer);
Assert.Equal(sourceOffset, barrier.Offset);
Assert.Equal(size, barrier.Size);
}
private static void AssertTrailingConsumerBarrier(
RecordedNativeCommand command,
CommandBuffer commands)
{
Assert.Equal(RecordedNativeCommandKind.MemoryBarrier, command.Kind);
Assert.Equal(commands, command.Commands);
MemoryBarrier2 barrier = command.MemoryBarrier;
Assert.Equal(StructureType.MemoryBarrier2, barrier.SType);
Assert.Equal(PipelineStageFlags2.AllTransferBit, barrier.SrcStageMask);
Assert.Equal(AccessFlags2.TransferWriteBit, barrier.SrcAccessMask);
Assert.Equal(
PipelineStageFlags2.VertexInputBit |
PipelineStageFlags2.VertexShaderBit |
PipelineStageFlags2.FragmentShaderBit |
PipelineStageFlags2.DrawIndirectBit,
barrier.DstStageMask);
Assert.Equal(
AccessFlags2.VertexAttributeReadBit |
AccessFlags2.IndexReadBit |
AccessFlags2.ShaderReadBit |
AccessFlags2.UniformReadBit |
AccessFlags2.IndirectCommandReadBit,
barrier.DstAccessMask);
}
private enum RecordedNativeCommandKind
{
BufferBarrier,
CopyBuffer,
MemoryBarrier,
}
private readonly record struct RecordedNativeCommand(
RecordedNativeCommandKind Kind,
CommandBuffer Commands,
VkBuffer Source,
VkBuffer Destination,
uint RegionCount,
BufferCopy Copy,
BufferMemoryBarrier2 BufferBarrier,
MemoryBarrier2 MemoryBarrier);
private sealed unsafe class RecordingNativeContext : INativeContext
{
private static RecordingNativeContext? s_active;
internal RecordingNativeContext()
{
Assert.Null(s_active);
s_active = this;
}
internal List<RecordedNativeCommand> Commands { get; } = [];
public nint GetProcAddress(string proc, int? slot = null) => proc switch
{
"vkCmdCopyBuffer" =>
(nint)(delegate* unmanaged<CommandBuffer, VkBuffer, VkBuffer, uint, BufferCopy*, void>)
&CaptureCopyBuffer,
"vkCmdPipelineBarrier2" =>
(nint)(delegate* unmanaged<CommandBuffer, DependencyInfo*, void>)
&CapturePipelineBarrier,
_ => (nint)(delegate* unmanaged<void>)&NoOp,
};
public bool TryGetProcAddress(string proc, out nint addr, int? slot = null)
{
addr = GetProcAddress(proc, slot);
return true;
}
public void Dispose()
{
if (ReferenceEquals(s_active, this))
s_active = null;
}
[UnmanagedCallersOnly]
private static void CaptureCopyBuffer(
CommandBuffer commands,
VkBuffer source,
VkBuffer destination,
uint regionCount,
BufferCopy* regions)
{
RecordingNativeContext active = s_active!;
active.Commands.Add(new RecordedNativeCommand(
RecordedNativeCommandKind.CopyBuffer,
commands,
source,
destination,
regionCount,
regionCount == 0 ? default : regions[0],
default,
default));
}
[UnmanagedCallersOnly]
private static void CapturePipelineBarrier(CommandBuffer commands, DependencyInfo* dependency)
{
RecordingNativeContext active = s_active!;
if (dependency->BufferMemoryBarrierCount == 1)
{
active.Commands.Add(new RecordedNativeCommand(
RecordedNativeCommandKind.BufferBarrier,
commands,
default,
default,
0,
default,
dependency->PBufferMemoryBarriers[0],
default));
return;
}
active.Commands.Add(new RecordedNativeCommand(
RecordedNativeCommandKind.MemoryBarrier,
commands,
default,
default,
0,
default,
default,
dependency->PMemoryBarriers[0]));
}
[UnmanagedCallersOnly]
private static void NoOp()
{
}
}
private static void AssertDepthEntry(
ImageMemoryBarrier2 barrier,
VkImage image,
PipelineStageFlags2 destinationStage,
AccessFlags2 destinationAccess)
{
Assert.Equal(StructureType.ImageMemoryBarrier2, barrier.SType);
Assert.Equal(PipelineStageFlags2.AllCommandsBit, barrier.SrcStageMask);
Assert.Equal(AccessFlags2.None, barrier.SrcAccessMask);
Assert.Equal(destinationStage, barrier.DstStageMask);
Assert.Equal(destinationAccess, barrier.DstAccessMask);
Assert.Equal(ImageLayout.Undefined, barrier.OldLayout);
Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, barrier.NewLayout);
Assert.Equal(image, barrier.Image);
AssertDepthStencilImage(barrier.SubresourceRange);
}
private static void AssertDepthExit(
ImageMemoryBarrier2 barrier,
VkImage image,
PipelineStageFlags2 sourceStage,
AccessFlags2 sourceAccess)
{
Assert.Equal(StructureType.ImageMemoryBarrier2, barrier.SType);
Assert.Equal(sourceStage, barrier.SrcStageMask);
Assert.Equal(sourceAccess, barrier.SrcAccessMask);
Assert.Equal(PipelineStageFlags2.FragmentShaderBit, barrier.DstStageMask);
Assert.Equal(AccessFlags2.ShaderReadBit, barrier.DstAccessMask);
Assert.Equal(ImageLayout.DepthStencilAttachmentOptimal, barrier.OldLayout);
Assert.Equal(ImageLayout.DepthStencilReadOnlyOptimal, barrier.NewLayout);
Assert.Equal(image, barrier.Image);
AssertDepthStencilImage(barrier.SubresourceRange);
}
private static void AssertColorImage(ImageSubresourceRange range)
{
Assert.Equal(ImageAspectFlags.ColorBit, range.AspectMask);
Assert.Equal(0u, range.BaseMipLevel);
Assert.Equal(1u, range.LevelCount);
Assert.Equal(0u, range.BaseArrayLayer);
Assert.Equal(1u, range.LayerCount);
}
private static void AssertDepthStencilImage(ImageSubresourceRange range)
{
Assert.Equal(ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit, range.AspectMask);
Assert.Equal(0u, range.BaseMipLevel);
Assert.Equal(Silk.NET.Vulkan.Vk.RemainingMipLevels, range.LevelCount);
Assert.Equal(0u, range.BaseArrayLayer);
Assert.Equal(Silk.NET.Vulkan.Vk.RemainingArrayLayers, range.LayerCount);
}
private static string MethodBody(string source, string startToken, string endToken)
{
int start = source.IndexOf(startToken, StringComparison.Ordinal);
int end = source.IndexOf(endToken, start, StringComparison.Ordinal);
Assert.True(start >= 0 && end > start);
return source[start..end];
}
private static string Normalize(string value) => value.Replace("\r\n", "\n", StringComparison.Ordinal);
private static string Source(params string[] path) =>
File.ReadAllText(Path.Combine([RepositoryRoot(), .. path]));
private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
directory = directory.Parent;
return directory?.FullName
?? throw new InvalidOperationException("Could not locate the repository root.");
}
}