using Silk.NET.Vulkan;
namespace AcDream.App.Rendering.Gpu.Vk;
///
/// Campaign V slice V6b: on Vulkan.
///
/// Images are device-local and filled through the staging path. 2D arrays
/// are allocated at full size and filled layer by layer, mirroring
/// ManagedGLTextureArray — which is why the upload queue tracks the
/// layout an image is in on entry rather than always discarding its contents.
///
///
/// is explicit rather than automatic
/// because the two backends genuinely cannot do it the same way, and the
/// contract says so. Uncompressed images get a vkCmdBlitImage chain.
/// Block-compressed images cannot: a compressed image is not a legal blit
/// destination, so this method throws and the caller supplies a CPU-built chain
/// through ( builds
/// it). That is a deliberate improvement rather than a limitation — the GL path
/// calls glGenerateMipmap on compressed arrays, whose result is
/// implementation-defined.
///
internal sealed unsafe class VulkanGpuTexture : IGpuTexture
{
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 VulkanGpuTexture(
Silk.NET.Vulkan.Vk vk,
Device device,
VulkanDeviceMemoryAllocator allocator,
VulkanUploadQueue uploads,
IGpuResourceRetirementQueue retirement,
VulkanDebugNames debugNames,
in GpuTextureDescription description,
int sampleCount = 1,
bool renderTarget = false,
// Fully qualified: in a parameter-default expression the simple name
// `Format` binds to this type's own GpuTextureFormat property first.
Format formatOverride = Silk.NET.Vulkan.Format.Undefined)
{
_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.Width);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.Height);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.LayerCount);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(description.MipLevelCount);
Name = description.Name;
Kind = description.Kind;
Format = description.Format;
Width = description.Width;
Height = description.Height;
LayerCount = description.LayerCount;
MipLevelCount = description.MipLevelCount;
SampleCount = sampleCount;
// Slice V6l: an offscreen target's DEPTH attachment takes the format the
// device already chose for the backbuffer, because a pipeline bakes one
// depth/stencil format and draws in both kinds of pass. The contract's
// Depth24Stencil8 means "a combined depth+stencil attachment"; which
// combined format that is belongs to the device that probed for it.
VkFormat = formatOverride != Silk.NET.Vulkan.Format.Undefined
? formatOverride
: VulkanTextureFormatMapping.FormatOf(description.Format);
Aspect = VulkanTextureFormatMapping.AspectOf(description.Format);
bool depthStencil = VulkanTextureFormatMapping.IsDepthStencil(description.Format);
ImageUsageFlags usage = depthStencil
? ImageUsageFlags.DepthStencilAttachmentBit
: ImageUsageFlags.SampledBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit;
if (renderTarget && !depthStencil)
usage |= ImageUsageFlags.ColorAttachmentBit;
if (sampleCount > 1)
{
// A multisampled image is never sampled or copied directly; it is
// resolved. Declaring TRANSIENT lets a tiler keep it in on-chip
// memory and never write it out at all.
usage = depthStencil
? ImageUsageFlags.DepthStencilAttachmentBit | ImageUsageFlags.TransientAttachmentBit
: ImageUsageFlags.ColorAttachmentBit | ImageUsageFlags.TransientAttachmentBit;
}
var create = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.Type2D,
Format = VkFormat,
Extent = new Extent3D((uint)description.Width, (uint)description.Height, 1),
MipLevels = (uint)description.MipLevelCount,
ArrayLayers = (uint)description.LayerCount,
Samples = VulkanTextureFormatMapping.SampleCountOf(sampleCount),
Tiling = ImageTiling.Optimal,
Usage = usage,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
};
VulkanInterop.Check(
_vk.CreateImage(_device, &create, null, out Image image),
$"vkCreateImage ('{description.Name}')");
Image = image;
try
{
_vk.GetImageMemoryRequirements(_device, image, out MemoryRequirements requirements);
_allocation = _allocator.Allocate(requirements, GpuMemoryResidency.DeviceLocal, description.Name);
VulkanInterop.Check(
_vk.BindImageMemory(_device, image, _allocation.Memory, _allocation.OffsetBytes),
$"vkBindImageMemory ('{description.Name}')");
var viewCreate = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
// Slice V6d: a texture that is not an attachment exists to be
// sampled, and every sampled view has to be layered because the
// global table's descriptor type is sampler2DArray. Attachments
// keep the literal view type of their kind.
ViewType = renderTarget
? VulkanTextureFormatMapping.ViewTypeOf(description.Kind)
: VulkanTextureFormatMapping.SampledViewTypeOf(description.Kind),
Format = VkFormat,
SubresourceRange = new ImageSubresourceRange
{
AspectMask = Aspect,
BaseMipLevel = 0,
LevelCount = (uint)description.MipLevelCount,
BaseArrayLayer = 0,
LayerCount = (uint)description.LayerCount,
},
};
VulkanInterop.Check(
_vk.CreateImageView(_device, &viewCreate, null, out ImageView view),
$"vkCreateImageView ('{description.Name}')");
View = view;
SampledView = view;
// Campaign V slice V6l: a colour render target needs TWO views.
//
// An ATTACHMENT view must be VK_IMAGE_VIEW_TYPE_2D, and the global
// texture table's descriptor array is declared sampler2DArray, so the
// attachment view cannot legally be registered into it — plan §5.5.7
// recorded that as invalid usage rather than a mismatch that samples
// oddly, and V6k made VulkanGpuDevice.RegisterTexture refuse it and
// name this fix. A second, LAYERED view over the same image is that
// fix: one image, one allocation, two ways of looking at it. Legal
// without any creation flag — a 2D_ARRAY view over an imageType-2D
// image with arrayLayers >= 1 is exactly what the spec permits.
if (renderTarget && !depthStencil)
{
viewCreate.ViewType = VulkanTextureFormatMapping.SampledViewTypeOf(description.Kind);
VulkanInterop.Check(
_vk.CreateImageView(_device, &viewCreate, null, out ImageView sampled),
$"vkCreateImageView ('{description.Name}', sampled)");
SampledView = sampled;
debugNames.NameImageView(sampled, $"{description.Name}-sampled-view");
}
}
catch
{
_vk.DestroyImage(_device, image, null);
throw;
}
debugNames.NameImage(image, description.Name);
debugNames.NameImageView(View, $"{description.Name}-view");
}
public string Name { get; }
public GpuTextureKind Kind { get; }
public GpuTextureFormat Format { get; }
public int Width { get; }
public int Height { get; }
public int LayerCount { get; }
public int MipLevelCount { get; }
internal int SampleCount { get; }
internal Image Image { get; }
/// The view a pass names as an attachment, and the only view a non-attachment has.
internal ImageView View { get; }
///
/// The view the global texture table samples. Identical to
/// for every ordinary texture; a second, LAYERED view for a colour render
/// target, because an attachment view is 2-D and the table's descriptor array
/// is sampler2DArray (slice V6l, plan §5.5.7).
///
internal ImageView SampledView { get; }
internal Format VkFormat { get; }
internal ImageAspectFlags Aspect { get; }
///
/// Layout the image is currently in, as far as the CPU-side record knows.
/// Starts UNDEFINED so the first upload may discard, and becomes
/// SHADER_READ_ONLY once anything has been written — which is what stops an
/// incremental array-layer fill from erasing the layers already there.
///
internal ImageLayout CurrentLayout { get; private set; } = ImageLayout.Undefined;
internal void MarkLayout(ImageLayout layout) => CurrentLayout = layout;
public void Upload(int mipLevel, int layer, ReadOnlySpan data)
{
ThrowIfDisposed();
ArgumentOutOfRangeException.ThrowIfNegative(mipLevel);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(mipLevel, MipLevelCount);
ArgumentOutOfRangeException.ThrowIfNegative(layer);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, LayerCount);
if (data.IsEmpty)
return;
(int width, int height) = VulkanTextureFormatMapping.LevelExtent(Width, Height, mipLevel);
int expected = VulkanTextureFormatMapping.LevelSizeBytes(Format, width, height);
if (data.Length < expected)
{
throw new ArgumentException(
$"Mip {mipLevel} of '{Name}' is {width}x{height} and needs {expected} bytes; " +
$"{data.Length} were supplied.",
nameof(data));
}
_uploads.StageImageWrite(Image, mipLevel, layer, width, height, CurrentLayout, data, Name);
CurrentLayout = ImageLayout.ShaderReadOnlyOptimal;
}
public void GenerateMipChain()
{
ThrowIfDisposed();
if (MipLevelCount <= 1)
return;
if (BlockCompressionCodec.IsBlockCompressed(Format))
{
throw new NotSupportedException(
$"'{Name}' is {Format}, and Vulkan cannot blit into a block-compressed image. " +
"Build the chain on the CPU with BlockCompressionMipChain and upload each level " +
"through Upload(mipLevel, layer, data). The GL path's reliance on driver-defined " +
"glGenerateMipmap for compressed arrays is deliberately not carried forward.");
}
_uploads.EnqueueMipBlit(Image, Width, Height, MipLevelCount, LayerCount, CurrentLayout);
CurrentLayout = ImageLayout.ShaderReadOnlyOptimal;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
Image image = Image;
ImageView view = View;
ImageView sampledView = SampledView;
VulkanAllocation allocation = _allocation;
_retirement.Retire(() =>
{
if (sampledView.Handle != view.Handle)
_vk.DestroyImageView(_device, sampledView, null);
_vk.DestroyImageView(_device, view, null);
_vk.DestroyImage(_device, image, null);
_allocator.Free(allocation);
});
}
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
}
///
/// Campaign V slice V6b: on Vulkan.
///
/// Immutable and de-duplicated by value at the device, because the set of
/// distinct samplers acdream uses is tiny — wrap or clamp, crossed with nearest
/// or linear. That is exactly what makes a combined image-sampler descriptor
/// table practical: a texture registered with two samplers occupies two slots,
/// the same way it holds two bindless handles today.
///
internal sealed unsafe class VulkanGpuSampler : IGpuSampler
{
private readonly Silk.NET.Vulkan.Vk _vk;
private readonly Device _device;
private readonly IGpuResourceRetirementQueue _retirement;
private bool _disposed;
internal VulkanGpuSampler(
Silk.NET.Vulkan.Vk vk,
Device device,
IGpuResourceRetirementQueue retirement,
VulkanDebugNames debugNames,
in GpuSamplerDescription description,
float maxSupportedAnisotropy)
{
_vk = vk ?? throw new ArgumentNullException(nameof(vk));
_device = device;
_retirement = retirement ?? throw new ArgumentNullException(nameof(retirement));
Description = description;
float anisotropy = Math.Clamp(description.MaxAnisotropy, 1f, Math.Max(1f, maxSupportedAnisotropy));
var create = new SamplerCreateInfo
{
SType = StructureType.SamplerCreateInfo,
MinFilter = VulkanTextureFormatMapping.FilterOf(description.MinFilter),
MagFilter = VulkanTextureFormatMapping.FilterOf(description.MagFilter),
MipmapMode = VulkanTextureFormatMapping.MipmapModeOf(description.MipFilter),
AddressModeU = VulkanTextureFormatMapping.AddressModeOf(description.AddressU),
AddressModeV = VulkanTextureFormatMapping.AddressModeOf(description.AddressV),
AddressModeW = VulkanTextureFormatMapping.AddressModeOf(description.AddressV),
AnisotropyEnable = anisotropy > 1f,
MaxAnisotropy = anisotropy,
MinLod = 0f,
// GpuMipFilter.None means "level 0 only", which Vulkan expresses as a
// zero-width LOD range rather than as a filter mode.
MaxLod = description.MipFilter == GpuMipFilter.None ? 0f : Silk.NET.Vulkan.Vk.LodClampNone,
BorderColor = BorderColor.FloatTransparentBlack,
CompareEnable = false,
UnnormalizedCoordinates = false,
};
VulkanInterop.Check(
_vk.CreateSampler(_device, &create, null, out Sampler sampler),
"vkCreateSampler");
Handle = sampler;
debugNames.NameSampler(
sampler,
$"sampler-{description.MinFilter}-{description.MipFilter}-{description.AddressU}");
}
public GpuSamplerDescription Description { get; }
internal Sampler Handle { get; }
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
Sampler handle = Handle;
_retirement.Retire(() => _vk.DestroySampler(_device, handle, null));
}
}