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)); } }