213 lines
8.2 KiB
C#
213 lines
8.2 KiB
C#
using Silk.NET.Vulkan;
|
|
using Buffer = Silk.NET.Vulkan.Buffer;
|
|
|
|
namespace AcDream.App.Rendering.Gpu.Vk;
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6a: <see cref="IGpuBuffer"/> on Vulkan.
|
|
///
|
|
/// <para><b>Host-writable buffers are written, not uploaded.</b> On a
|
|
/// <see cref="GpuMemoryResidency.HostWritable"/> buffer <see cref="Upload"/> is
|
|
/// a <c>memcpy</c> into memory the GPU already reads — the driver validation,
|
|
/// the copy and the rename tracking that <c>glBufferSubData</c> pays for simply
|
|
/// do not happen. On a device-local buffer it stages through
|
|
/// <see cref="VulkanUploadQueue"/> instead, which is the only case that costs a
|
|
/// transfer.</para>
|
|
///
|
|
/// <para><b>Usage flags are named at creation, so they are named generously.</b>
|
|
/// Vulkan requires every consumption to be declared up front and the RHI
|
|
/// contract puts <see cref="Upload"/> and <see cref="CopyTo"/> on every buffer,
|
|
/// so transfer source and destination are always included. They cost nothing on
|
|
/// any implementation acdream targets and the alternative — inferring them from
|
|
/// how a buffer happens to be used later — is how a driver error appears months
|
|
/// after the code that caused it.</para>
|
|
///
|
|
/// <para>Disposal routes through the device's retirement queue rather than
|
|
/// destroying immediately, so a submitted frame that still reads this buffer
|
|
/// keeps valid memory.</para>
|
|
/// </summary>
|
|
internal sealed unsafe class VulkanGpuBuffer : IGpuBuffer
|
|
{
|
|
private readonly Silk.NET.Vulkan.Vk _vk;
|
|
private readonly Device _device;
|
|
private readonly VulkanDeviceMemoryAllocator _allocator;
|
|
private readonly VulkanUploadQueue _uploads;
|
|
private readonly IGpuResourceRetirementQueue _retirement;
|
|
private readonly VulkanAllocation _allocation;
|
|
private bool _disposed;
|
|
|
|
internal VulkanGpuBuffer(
|
|
Silk.NET.Vulkan.Vk vk,
|
|
Device device,
|
|
VulkanDeviceMemoryAllocator allocator,
|
|
VulkanUploadQueue uploads,
|
|
IGpuResourceRetirementQueue retirement,
|
|
VulkanDebugNames debugNames,
|
|
in GpuBufferDescription description)
|
|
{
|
|
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
|
|
_device = device;
|
|
_allocator = allocator ?? throw new ArgumentNullException(nameof(allocator));
|
|
_uploads = uploads ?? throw new ArgumentNullException(nameof(uploads));
|
|
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(description.Name);
|
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.SizeBytes);
|
|
|
|
Name = description.Name;
|
|
SizeBytes = description.SizeBytes;
|
|
Usage = description.Usage;
|
|
Residency = description.Residency;
|
|
|
|
var create = new BufferCreateInfo
|
|
{
|
|
SType = StructureType.BufferCreateInfo,
|
|
Size = (ulong)description.SizeBytes,
|
|
Usage = UsageFlagsOf(description.Usage),
|
|
SharingMode = SharingMode.Exclusive,
|
|
};
|
|
VulkanInterop.Check(
|
|
_vk.CreateBuffer(_device, &create, null, out Buffer handle),
|
|
$"vkCreateBuffer ('{description.Name}')");
|
|
Handle = handle;
|
|
|
|
try
|
|
{
|
|
_vk.GetBufferMemoryRequirements(_device, handle, out MemoryRequirements requirements);
|
|
_allocation = _allocator.Allocate(requirements, description.Residency, description.Name);
|
|
VulkanInterop.Check(
|
|
_vk.BindBufferMemory(_device, handle, _allocation.Memory, _allocation.OffsetBytes),
|
|
$"vkBindBufferMemory ('{description.Name}')");
|
|
}
|
|
catch
|
|
{
|
|
_vk.DestroyBuffer(_device, handle, null);
|
|
throw;
|
|
}
|
|
|
|
debugNames.NameBuffer(handle, description.Name);
|
|
}
|
|
|
|
public string Name { get; }
|
|
public long SizeBytes { get; }
|
|
public GpuBufferUsage Usage { get; }
|
|
public GpuMemoryResidency Residency { get; }
|
|
public bool HostWritesAreCoherent =>
|
|
_allocation.MemoryProperties.HasFlag(MemoryPropertyFlags.HostCoherentBit);
|
|
|
|
internal Buffer Handle { get; }
|
|
|
|
/// <summary>True when this buffer's memory is persistently mapped and directly writable.</summary>
|
|
internal bool IsMapped => _allocation.IsMapped;
|
|
|
|
/// <summary>The buffer's whole mapped range. Only valid on host-visible residency.</summary>
|
|
internal Span<byte> MappedSpan => _allocation.AsSpan()[..checked((int)SizeBytes)];
|
|
|
|
public void Upload(long offsetBytes, ReadOnlySpan<byte> data)
|
|
{
|
|
ThrowIfDisposed();
|
|
ArgumentOutOfRangeException.ThrowIfNegative(offsetBytes);
|
|
if (data.IsEmpty)
|
|
return;
|
|
if (offsetBytes + data.Length > SizeBytes)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(data),
|
|
$"Writing {data.Length} bytes at offset {offsetBytes} exceeds " +
|
|
$"'{Name}' ({SizeBytes} bytes).");
|
|
}
|
|
|
|
if (_allocation.IsMapped)
|
|
{
|
|
data.CopyTo(MappedSpan.Slice((int)offsetBytes, data.Length));
|
|
return;
|
|
}
|
|
|
|
_uploads.StageBufferWrite(Handle, (ulong)offsetBytes, data, Name);
|
|
}
|
|
|
|
public void CopyTo(
|
|
IGpuBuffer destination,
|
|
long sourceOffsetBytes,
|
|
long destinationOffsetBytes,
|
|
long byteCount)
|
|
{
|
|
ThrowIfDisposed();
|
|
ArgumentNullException.ThrowIfNull(destination);
|
|
if (destination is not VulkanGpuBuffer target)
|
|
{
|
|
throw new ArgumentException(
|
|
"The Vulkan backend can only copy into a Vulkan buffer.",
|
|
nameof(destination));
|
|
}
|
|
|
|
ArgumentOutOfRangeException.ThrowIfNegative(byteCount);
|
|
if (byteCount == 0)
|
|
return;
|
|
if (sourceOffsetBytes + byteCount > SizeBytes)
|
|
throw new ArgumentOutOfRangeException(nameof(byteCount), $"The copy reads past the end of '{Name}'.");
|
|
if (destinationOffsetBytes + byteCount > target.SizeBytes)
|
|
throw new ArgumentOutOfRangeException(nameof(byteCount), $"The copy writes past the end of '{target.Name}'.");
|
|
|
|
_uploads.EnqueueBufferCopy(
|
|
Handle,
|
|
target.Handle,
|
|
(ulong)sourceOffsetBytes,
|
|
(ulong)destinationOffsetBytes,
|
|
(ulong)byteCount);
|
|
}
|
|
|
|
public void Read(long offsetBytes, Span<byte> destination)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (Residency != GpuMemoryResidency.HostReadable)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"'{Name}' has {Residency} residency; only HostReadable buffers can be read back. " +
|
|
"This path is diagnostics-only by design.");
|
|
}
|
|
|
|
if (destination.IsEmpty)
|
|
return;
|
|
if (offsetBytes + destination.Length > SizeBytes)
|
|
throw new ArgumentOutOfRangeException(nameof(destination), $"The read runs past the end of '{Name}'.");
|
|
|
|
MappedSpan.Slice((int)offsetBytes, destination.Length).CopyTo(destination);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps <see cref="GpuBufferUsage"/> onto Vulkan usage bits. Transfer source
|
|
/// and destination are unconditional — see the class remarks.
|
|
/// </summary>
|
|
internal static BufferUsageFlags UsageFlagsOf(GpuBufferUsage usage)
|
|
{
|
|
BufferUsageFlags flags = BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit;
|
|
if (usage.HasFlag(GpuBufferUsage.Vertex))
|
|
flags |= BufferUsageFlags.VertexBufferBit;
|
|
if (usage.HasFlag(GpuBufferUsage.Index))
|
|
flags |= BufferUsageFlags.IndexBufferBit;
|
|
if (usage.HasFlag(GpuBufferUsage.Storage))
|
|
flags |= BufferUsageFlags.StorageBufferBit;
|
|
if (usage.HasFlag(GpuBufferUsage.Uniform))
|
|
flags |= BufferUsageFlags.UniformBufferBit;
|
|
if (usage.HasFlag(GpuBufferUsage.Indirect))
|
|
flags |= BufferUsageFlags.IndirectBufferBit;
|
|
return flags;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_disposed = true;
|
|
|
|
Buffer handle = Handle;
|
|
VulkanAllocation allocation = _allocation;
|
|
_retirement.Retire(() =>
|
|
{
|
|
_vk.DestroyBuffer(_device, handle, null);
|
|
_allocator.Free(allocation);
|
|
});
|
|
}
|
|
|
|
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
|
|
}
|