fix: land reviewed GPU synchronization repairs with verification ledger
This commit is contained in:
parent
34d050fabc
commit
73de7403c3
8 changed files with 881 additions and 99 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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->B->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,
|
||||
|
|
|
|||
|
|
@ -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, ®ion);",
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue