From c9d018db8e7a9a80ecceb52f15c17c48c0d8016a Mon Sep 17 00:00:00 2001 From: lilp <79549074+FelipeGFA@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:54:52 -0300 Subject: [PATCH] Vulkan: fix guest storage image and render-state handling (#332) --- src/SharpEmu.Libs/Agc/AgcExports.cs | 35 +- .../VideoOut/BoundedByteArrayPool.cs | 122 +++++ .../VideoOut/GuestBlendStateNormalizer.cs | 38 ++ .../VideoOut/GuestDepthExtentResolver.cs | 67 +++ .../VideoOut/VulkanVideoPresenter.cs | 508 +++++++++++++++--- .../Gen5SpirvTranslator.cs | 154 ++++-- .../SpirvModuleBuilder.cs | 1 + .../Gen5ShaderTranslator.cs | 28 +- 8 files changed, 806 insertions(+), 147 deletions(-) create mode 100644 src/SharpEmu.Libs/VideoOut/BoundedByteArrayPool.cs create mode 100644 src/SharpEmu.Libs/VideoOut/GuestBlendStateNormalizer.cs create mode 100644 src/SharpEmu.Libs/VideoOut/GuestDepthExtentResolver.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 1a916816..250bb91d 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -5779,7 +5779,9 @@ public static partial class AgcExports textures.Add(new TranslatedImageBinding( texture, - Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode), + Gen5ShaderTranslator.RequiresStorageImage( + binding, + exportEvaluation.ImageBindings), binding.MipLevel ?? 0, binding.SamplerDescriptor)); } @@ -6102,17 +6104,22 @@ public static partial class AgcExports _graphicsShaderCache.TryAdd(shaderKey, compiled); } + var imageBindings = pixelEvaluation.ImageBindings + .Concat(exportEvaluation.ImageBindings) + .ToArray(); var textures = new List( pixelEvaluation.ImageBindings.Count + exportEvaluation.ImageBindings.Count); if (!TryAppendTranslatedImageBindings( pixelEvaluation.ImageBindings, + imageBindings, textures, pixelShaderAddress, exportShaderAddress, out error) || !TryAppendTranslatedImageBindings( exportEvaluation.ImageBindings, + imageBindings, textures, pixelShaderAddress, exportShaderAddress, @@ -6191,6 +6198,7 @@ public static partial class AgcExports private static bool TryAppendTranslatedImageBindings( IReadOnlyList bindings, + IReadOnlyList stageBindings, List textures, ulong pixelShaderAddress, ulong exportShaderAddress, @@ -6215,8 +6223,9 @@ public static partial class AgcExports 0, 1, 1, Gen5TextureFormatR8G8B8A8Unorm, 0, 0, 0, 0, 0, 1, 0xFAC); } - var isStorage = - Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); + var isStorage = Gen5ShaderTranslator.RequiresStorageImage( + binding, + stageBindings); if (_traceAgcShader || _tracePixelShaderAddress == pixelShaderAddress) { Console.Error.WriteLine( @@ -7374,9 +7383,11 @@ public static partial class AgcExports /// private static void ReturnPooledEvaluationArrays(Gen5ShaderEvaluation evaluation) { + var returned = new HashSet( + System.Collections.Generic.ReferenceEqualityComparer.Instance); foreach (var binding in evaluation.GlobalMemoryBindings) { - if (binding.DataPooled) + if (binding.DataPooled && returned.Add(binding.Data)) { VulkanVideoPresenter.GuestDataPool.Return(binding.Data); } @@ -7386,7 +7397,7 @@ public static partial class AgcExports { foreach (var binding in vertexInputs) { - if (binding.DataPooled) + if (binding.DataPooled && returned.Add(binding.Data)) { VulkanVideoPresenter.GuestDataPool.Return(binding.Data); } @@ -7406,11 +7417,13 @@ public static partial class AgcExports bool vertex, bool index) { + var returned = new HashSet( + System.Collections.Generic.ReferenceEqualityComparer.Instance); if (globals) { foreach (var binding in draw.GlobalMemoryBindings) { - if (binding.DataPooled) + if (binding.DataPooled && returned.Add(binding.Data)) { VulkanVideoPresenter.GuestDataPool.Return(binding.Data); } @@ -7421,14 +7434,15 @@ public static partial class AgcExports { foreach (var binding in draw.VertexInputs) { - if (binding.DataPooled) + if (binding.DataPooled && returned.Add(binding.Data)) { VulkanVideoPresenter.GuestDataPool.Return(binding.Data); } } } - if (index && draw.IndexBuffer is { Pooled: true } indexBuffer) + if (index && draw.IndexBuffer is { Pooled: true } indexBuffer && + returned.Add(indexBuffer.Data)) { VulkanVideoPresenter.GuestDataPool.Return(indexBuffer.Data); } @@ -8522,7 +8536,8 @@ public static partial class AgcExports var hasStorageBinding = false; foreach (var binding in bindings) { - var isStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); + var isStorage = Gen5ShaderTranslator.RequiresStorageImage(binding, bindings); + var writesStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); var descriptorValid = TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture); if (!descriptorValid) { @@ -8543,7 +8558,7 @@ public static partial class AgcExports $"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" + $"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" + $"{descriptorState}/{ProbeTexture(ctx, texture)}"); - if (isStorage && descriptorValid && texture.Address != 0) + if (writesStorage && descriptorValid && texture.Address != 0) { gpuState.ComputeImageWriters[texture.Address] = new ComputeImageWriter( sequence, diff --git a/src/SharpEmu.Libs/VideoOut/BoundedByteArrayPool.cs b/src/SharpEmu.Libs/VideoOut/BoundedByteArrayPool.cs new file mode 100644 index 00000000..0c7e1e50 --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/BoundedByteArrayPool.cs @@ -0,0 +1,122 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers; +using System.Numerics; + +namespace SharpEmu.Libs.VideoOut; + +internal sealed class BoundedByteArrayPool : ArrayPool +{ + private readonly object _gate = new(); + private readonly int _maxArrayLength; + private readonly ulong _maxCachedBytes; + private readonly int _maxArraysPerBucket; + private readonly Dictionary> _cachedByBucket = []; + private readonly HashSet _leases = + new(System.Collections.Generic.ReferenceEqualityComparer.Instance); + private ulong _cachedBytes; + + public BoundedByteArrayPool( + int maxArrayLength, + ulong maxCachedBytes, + int maxArraysPerBucket) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength); + ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket); + _maxArrayLength = maxArrayLength; + _maxCachedBytes = maxCachedBytes; + _maxArraysPerBucket = maxArraysPerBucket; + } + + public override byte[] Rent(int minimumLength) + { + ArgumentOutOfRangeException.ThrowIfNegative(minimumLength); + var length = GetAllocationLength(minimumLength); + byte[]? array = null; + lock (_gate) + { + if (length <= _maxArrayLength && + _cachedByBucket.TryGetValue(length, out var bucket) && + bucket.TryPop(out array)) + { + _cachedBytes -= (ulong)array.LongLength; + } + + array ??= new byte[length]; + _leases.Add(array); + } + + return array; + } + + public override void Return(byte[] array, bool clearArray = false) + { + ArgumentNullException.ThrowIfNull(array); + lock (_gate) + { + if (!_leases.Remove(array)) + { + return; + } + } + + if (clearArray) + { + Array.Clear(array); + } + + lock (_gate) + { + if (array.Length > _maxArrayLength || + !IsBucketLength(array.Length) || + (ulong)array.LongLength > _maxCachedBytes - + Math.Min(_cachedBytes, _maxCachedBytes)) + { + return; + } + + if (!_cachedByBucket.TryGetValue(array.Length, out var bucket)) + { + bucket = new Stack(); + _cachedByBucket.Add(array.Length, bucket); + } + + if (bucket.Count >= _maxArraysPerBucket) + { + return; + } + + bucket.Push(array); + _cachedBytes += (ulong)array.LongLength; + } + } + + public void Trim() + { + lock (_gate) + { + _cachedByBucket.Clear(); + _cachedBytes = 0; + } + } + + private int GetAllocationLength(int minimumLength) + { + if (minimumLength <= 16) + { + return 16; + } + + if (minimumLength > _maxArrayLength) + { + return minimumLength; + } + + return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength)); + } + + private static bool IsBucketLength(int length) => + length >= 16 && (length & (length - 1)) == 0; +} diff --git a/src/SharpEmu.Libs/VideoOut/GuestBlendStateNormalizer.cs b/src/SharpEmu.Libs/VideoOut/GuestBlendStateNormalizer.cs new file mode 100644 index 00000000..cc8be6be --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/GuestBlendStateNormalizer.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Gpu; + +namespace SharpEmu.Libs.VideoOut; + +internal static class GuestBlendStateNormalizer +{ + public static GuestBlendState[] NormalizeIntegerAttachments( + IReadOnlyList blends, + IReadOnlyList integerAttachments, + out int normalizedCount) + { + if (blends.Count != integerAttachments.Count) + { + throw new ArgumentException( + "color attachment and blend-state counts must match", + nameof(integerAttachments)); + } + + var normalized = new GuestBlendState[blends.Count]; + normalizedCount = 0; + for (var index = 0; index < blends.Count; index++) + { + var blend = blends[index]; + if (integerAttachments[index] && blend.Enable) + { + blend = blend with { Enable = false }; + normalizedCount++; + } + + normalized[index] = blend; + } + + return normalized; + } +} diff --git a/src/SharpEmu.Libs/VideoOut/GuestDepthExtentResolver.cs b/src/SharpEmu.Libs/VideoOut/GuestDepthExtentResolver.cs new file mode 100644 index 00000000..dfcc6caa --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/GuestDepthExtentResolver.cs @@ -0,0 +1,67 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Gpu; + +namespace SharpEmu.Libs.VideoOut; + +internal enum GuestDepthExtentResolutionKind +{ + Exact, + TextureAlias, + StaleOneByOne, + Mismatch, +} + +internal readonly record struct GuestDepthExtentResolution( + GuestDepthExtentResolutionKind Kind, + uint Width, + uint Height) +{ + public bool IsUsable => Kind != GuestDepthExtentResolutionKind.Mismatch; +} + +internal static class GuestDepthExtentResolver +{ + public static GuestDepthExtentResolution Resolve( + GuestDepthTarget depth, + uint colorWidth, + uint colorHeight, + IReadOnlyList textures) + { + if (depth.Width >= colorWidth && depth.Height >= colorHeight) + { + return new GuestDepthExtentResolution( + GuestDepthExtentResolutionKind.Exact, + depth.Width, + depth.Height); + } + + var matchingTexture = textures.FirstOrDefault(texture => + (texture.Address == depth.Address || + texture.Address == depth.ReadAddress || + texture.Address == depth.WriteAddress) && + texture.Width >= colorWidth && + texture.Height >= colorHeight); + if (matchingTexture is not null) + { + return new GuestDepthExtentResolution( + GuestDepthExtentResolutionKind.TextureAlias, + matchingTexture.Width, + matchingTexture.Height); + } + + if (depth.Width == 1 && depth.Height == 1) + { + return new GuestDepthExtentResolution( + GuestDepthExtentResolutionKind.StaleOneByOne, + colorWidth, + colorHeight); + } + + return new GuestDepthExtentResolution( + GuestDepthExtentResolutionKind.Mismatch, + depth.Width, + depth.Height); + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 7d72e54f..8b194973 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -15,6 +15,7 @@ using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; using Silk.NET.Vulkan.Extensions.EXT; using Silk.NET.Windowing; +using System.Buffers.Binary; using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; @@ -104,6 +105,233 @@ internal static unsafe class VulkanVideoPresenter // always takes its dimensions from the native child control instead. private const uint DefaultWindowWidth = 1920; private const uint DefaultWindowHeight = 1080; + + internal enum StorageImageComponentKind + { + Float, + Sint, + Uint, + } + + internal readonly record struct SpirvStorageImageContract( + SpirvImageFormat Format, + StorageImageComponentKind ComponentKind); + + internal static bool TryReadSpirvStorageImageContracts( + ReadOnlySpan spirv, + out SpirvStorageImageContract[] contracts, + out string error) + { + contracts = []; + error = string.Empty; + if (spirv.Length < 5 * sizeof(uint) || + BinaryPrimitives.ReadUInt32LittleEndian(spirv) != 0x07230203u) + { + error = "invalid-spirv-header"; + return false; + } + + var componentTypes = new Dictionary(); + var storageImageTypes = new Dictionary(); + var uniformConstantPointers = new Dictionary(); + var uniformConstantVariables = new List<(uint Id, uint PointerType)>(); + var bindings = new Dictionary(); + for (var offset = 5 * sizeof(uint); offset < spirv.Length;) + { + var instruction = BinaryPrimitives.ReadUInt32LittleEndian( + spirv.Slice(offset, sizeof(uint))); + var wordCount = checked((int)(instruction >> 16)); + var byteCount = checked(wordCount * sizeof(uint)); + if (wordCount == 0 || offset + byteCount > spirv.Length) + { + error = "invalid-spirv-instruction-size"; + return false; + } + + switch ((SpirvOp)(instruction & 0xFFFFu)) + { + case SpirvOp.TypeInt when wordCount >= 4: + componentTypes[ReadSpirvWord(spirv, offset, 1)] = + ReadSpirvWord(spirv, offset, 3) != 0 + ? StorageImageComponentKind.Sint + : StorageImageComponentKind.Uint; + break; + case SpirvOp.TypeFloat when wordCount >= 3: + componentTypes[ReadSpirvWord(spirv, offset, 1)] = + StorageImageComponentKind.Float; + break; + case SpirvOp.TypeImage when wordCount >= 9 && + ReadSpirvWord(spirv, offset, 7) == 2: + var imageType = ReadSpirvWord(spirv, offset, 1); + var componentType = ReadSpirvWord(spirv, offset, 2); + if (!componentTypes.TryGetValue(componentType, out var componentKind)) + { + error = $"unknown-storage-component-type({componentType})"; + return false; + } + + storageImageTypes[imageType] = new SpirvStorageImageContract( + (SpirvImageFormat)ReadSpirvWord(spirv, offset, 8), + componentKind); + break; + case SpirvOp.TypePointer when wordCount >= 4 && + ReadSpirvWord(spirv, offset, 2) == + (uint)SpirvStorageClass.UniformConstant: + uniformConstantPointers[ReadSpirvWord(spirv, offset, 1)] = + ReadSpirvWord(spirv, offset, 3); + break; + case SpirvOp.Variable when wordCount >= 4 && + ReadSpirvWord(spirv, offset, 3) == + (uint)SpirvStorageClass.UniformConstant: + uniformConstantVariables.Add(( + ReadSpirvWord(spirv, offset, 2), + ReadSpirvWord(spirv, offset, 1))); + break; + case SpirvOp.Decorate when wordCount >= 4 && + ReadSpirvWord(spirv, offset, 2) == + (uint)SpirvDecoration.Binding: + bindings[ReadSpirvWord(spirv, offset, 1)] = + ReadSpirvWord(spirv, offset, 3); + break; + } + + offset += byteCount; + } + + var result = new List<(uint Binding, SpirvStorageImageContract Contract)>(); + foreach (var variable in uniformConstantVariables) + { + if (!uniformConstantPointers.TryGetValue(variable.PointerType, out var imageType) || + !storageImageTypes.TryGetValue(imageType, out var contract)) + { + continue; + } + + if (!bindings.TryGetValue(variable.Id, out var binding) || + result.Any(entry => entry.Binding == binding)) + { + error = $"invalid-storage-image-binding({variable.Id})"; + return false; + } + + result.Add((binding, contract)); + } + + contracts = result + .OrderBy(static entry => entry.Binding) + .Select(static entry => entry.Contract) + .ToArray(); + return true; + } + + private static uint ReadSpirvWord( + ReadOnlySpan spirv, + int instructionOffset, + int wordIndex) => + BinaryPrimitives.ReadUInt32LittleEndian( + spirv.Slice( + instructionOffset + wordIndex * sizeof(uint), + sizeof(uint))); + + internal static bool TryValidateStorageImageContract( + SpirvStorageImageContract shaderContract, + uint guestFormat, + uint guestNumberType, + bool supportsStorage, + out Format vulkanFormat, + out string error) + { + vulkanFormat = Presenter.GetStorageImageFormat( + Presenter.GetTextureFormat(guestFormat, guestNumberType)); + var guestComponentKind = guestNumberType switch + { + 4 => StorageImageComponentKind.Uint, + 5 => StorageImageComponentKind.Sint, + _ => StorageImageComponentKind.Float, + }; + if (shaderContract.ComponentKind != guestComponentKind) + { + error = $"component-kind-mismatch(spirv={shaderContract.ComponentKind}," + + $"guest={guestComponentKind})"; + return false; + } + + if (shaderContract.Format != SpirvImageFormat.Unknown && + TryGetVulkanStorageImageFormat(shaderContract.Format, out var typedFormat) && + typedFormat != vulkanFormat) + { + error = $"typed-format-mismatch(spirv={shaderContract.Format}/{typedFormat}," + + $"guest={guestFormat}/num={guestNumberType},vk={vulkanFormat})"; + return false; + } + + if (shaderContract.Format != SpirvImageFormat.Unknown && + !TryGetVulkanStorageImageFormat(shaderContract.Format, out _)) + { + error = $"unsupported-spirv-storage-format({shaderContract.Format})"; + return false; + } + + if (!supportsStorage) + { + error = $"vulkan-storage-feature-missing(vk={vulkanFormat})"; + return false; + } + + error = string.Empty; + return true; + } + + private static bool TryGetVulkanStorageImageFormat( + SpirvImageFormat format, + out Format vulkanFormat) + { + vulkanFormat = format switch + { + SpirvImageFormat.Rgba32f => Format.R32G32B32A32Sfloat, + SpirvImageFormat.Rgba16f => Format.R16G16B16A16Sfloat, + SpirvImageFormat.R32f => Format.R32Sfloat, + SpirvImageFormat.Rgba8 => Format.R8G8B8A8Unorm, + SpirvImageFormat.Rgba8Snorm => Format.R8G8B8A8SNorm, + SpirvImageFormat.Rg32f => Format.R32G32Sfloat, + SpirvImageFormat.Rg16f => Format.R16G16Sfloat, + SpirvImageFormat.R11fG11fB10f => Format.B10G11R11UfloatPack32, + SpirvImageFormat.R16f => Format.R16Sfloat, + SpirvImageFormat.Rgba16 => Format.R16G16B16A16Unorm, + SpirvImageFormat.Rgb10A2 => Format.A2B10G10R10UnormPack32, + SpirvImageFormat.Rg16 => Format.R16G16Unorm, + SpirvImageFormat.Rg8 => Format.R8G8Unorm, + SpirvImageFormat.R16 => Format.R16Unorm, + SpirvImageFormat.R8 => Format.R8Unorm, + SpirvImageFormat.Rgba16Snorm => Format.R16G16B16A16SNorm, + SpirvImageFormat.Rg16Snorm => Format.R16G16SNorm, + SpirvImageFormat.Rg8Snorm => Format.R8G8SNorm, + SpirvImageFormat.R16Snorm => Format.R16SNorm, + SpirvImageFormat.R8Snorm => Format.R8SNorm, + SpirvImageFormat.Rgba32i => Format.R32G32B32A32Sint, + SpirvImageFormat.Rgba16i => Format.R16G16B16A16Sint, + SpirvImageFormat.Rgba8i => Format.R8G8B8A8Sint, + SpirvImageFormat.R32i => Format.R32Sint, + SpirvImageFormat.Rg32i => Format.R32G32Sint, + SpirvImageFormat.Rg16i => Format.R16G16Sint, + SpirvImageFormat.Rg8i => Format.R8G8Sint, + SpirvImageFormat.R16i => Format.R16Sint, + SpirvImageFormat.R8i => Format.R8Sint, + SpirvImageFormat.Rgba32ui => Format.R32G32B32A32Uint, + SpirvImageFormat.Rgba16ui => Format.R16G16B16A16Uint, + SpirvImageFormat.Rgba8ui => Format.R8G8B8A8Uint, + SpirvImageFormat.R32ui => Format.R32Uint, + SpirvImageFormat.Rgb10A2ui => Format.A2B10G10R10UintPack32, + SpirvImageFormat.Rg32ui => Format.R32G32Uint, + SpirvImageFormat.Rg16ui => Format.R16G16Uint, + SpirvImageFormat.Rg8ui => Format.R8G8Uint, + SpirvImageFormat.R16ui => Format.R16Uint, + SpirvImageFormat.R8ui => Format.R8Uint, + _ => Format.Undefined, + }; + return vulkanFormat != Format.Undefined; + } + // Vulkan's portable upper bound for minStorageBufferOffsetAlignment is // 256 bytes. Using that fixed power of two (instead of racing the render // thread's physical-device query) gives shader translation and descriptor @@ -114,10 +342,10 @@ internal static unsafe class VulkanVideoPresenter // trims and repartitions those large arrays aggressively under GC load, // causing hundreds of MiB/s of replacement byte[] allocations. Keep a // bounded, non-shared pool for AGC-to-presenter ownership transfers. - internal static System.Buffers.ArrayPool GuestDataPool { get; } = - System.Buffers.ArrayPool.Create( - maxArrayLength: 16 * 1024 * 1024, - maxArraysPerBucket: 96); + internal static BoundedByteArrayPool GuestDataPool { get; } = new( + maxArrayLength: 16 * 1024 * 1024, + maxCachedBytes: 256UL * 1024 * 1024, + maxArraysPerBucket: 8); // The pending queue and per-render drain budget bound how much guest GPU // work can be buffered ahead of the presenter. Draws are batched into // shared command buffers, so draining a large batch per render tick is @@ -2813,6 +3041,7 @@ internal static unsafe class VulkanVideoPresenter public bool InitialUploadPending; public bool IsCpuBacked; public ulong CpuContentFingerprint; + public bool SupportsStorageUsage; } private sealed record PendingGuestSubmission( @@ -5112,9 +5341,11 @@ internal static unsafe class VulkanVideoPresenter /// private static void ReturnPooledGuestData(VulkanTranslatedGuestDraw draw) { + var returned = new HashSet( + System.Collections.Generic.ReferenceEqualityComparer.Instance); foreach (var buffer in draw.GlobalMemoryBuffers) { - if (buffer.Pooled) + if (buffer.Pooled && returned.Add(buffer.Data)) { GuestDataPool.Return(buffer.Data); } @@ -5122,13 +5353,14 @@ internal static unsafe class VulkanVideoPresenter foreach (var buffer in draw.VertexBuffers) { - if (buffer.Pooled) + if (buffer.Pooled && returned.Add(buffer.Data)) { GuestDataPool.Return(buffer.Data); } } - if (draw.IndexBuffer is { Pooled: true } indexBuffer) + if (draw.IndexBuffer is { Pooled: true } indexBuffer && + returned.Add(indexBuffer.Data)) { GuestDataPool.Return(indexBuffer.Data); } @@ -7030,9 +7262,16 @@ internal static unsafe class VulkanVideoPresenter } var guestImage = ResolveStorageGuestImage(texture); - var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); + var vkFormat = GetStorageImageFormat( + GetTextureFormat(texture.Format, texture.NumberType)); + if (!SupportsStorageImage(vkFormat)) + { + throw new InvalidOperationException( + $"Storage image format {vkFormat} is unsupported for guest " + + $"format={texture.Format}/num={texture.NumberType}."); + } var selectedMipLevel = GetStorageMipLevel(texture); - var view = GetOrCreateGuestImageView( + var view = GetOrCreateGuestImageIdentityView( guestImage, vkFormat, selectedMipLevel, @@ -7109,7 +7348,14 @@ internal static unsafe class VulkanVideoPresenter { var width = Math.Max(texture.Width, 1); var height = Math.Max(texture.Height, 1); - var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); + var vkFormat = GetStorageImageFormat( + GetTextureFormat(texture.Format, texture.NumberType)); + if (!SupportsStorageImage(vkFormat)) + { + throw new InvalidOperationException( + $"Storage scratch format {vkFormat} is unsupported for guest " + + $"format={texture.Format}/num={texture.NumberType}."); + } var imageInfo = new ImageCreateInfo { SType = StructureType.ImageCreateInfo, @@ -7177,6 +7423,7 @@ internal static unsafe class VulkanVideoPresenter Image = image, Memory = memory, View = view, + SupportsStorageUsage = true, }; return new TextureResource @@ -7203,7 +7450,8 @@ internal static unsafe class VulkanVideoPresenter throw new InvalidOperationException("Storage image has no guest address."); } - var format = GetTextureFormat(texture.Format, texture.NumberType); + var format = GetStorageImageFormat( + GetTextureFormat(texture.Format, texture.NumberType)); var guestImage = GetOrCreateGuestImage( new GuestRenderTarget( texture.Address, @@ -7212,7 +7460,8 @@ internal static unsafe class VulkanVideoPresenter texture.Format, texture.NumberType, texture.ResourceMipLevels), - format); + format, + requiresStorage: true); var selectedMipLevel = GetStorageMipLevel(texture); if (selectedMipLevel >= guestImage.MipLevels) { @@ -7284,6 +7533,8 @@ internal static unsafe class VulkanVideoPresenter $"{TextureDebugName(texture, vkFormat)} staging"); var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat); + var supportsStorageUsage = supportsAttachmentUsage && + SupportsStorageImage(vkFormat); var imageInfo = new ImageCreateInfo { SType = StructureType.ImageCreateInfo, @@ -7301,7 +7552,7 @@ internal static unsafe class VulkanVideoPresenter ? ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit | ImageUsageFlags.ColorAttachmentBit | - ImageUsageFlags.StorageBit | + (supportsStorageUsage ? ImageUsageFlags.StorageBit : (ImageUsageFlags)0) | ImageUsageFlags.TransferSrcBit : ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit, SharingMode = SharingMode.Exclusive, @@ -7370,6 +7621,7 @@ internal static unsafe class VulkanVideoPresenter InitialUploadPending = true, IsCpuBacked = true, CpuContentFingerprint = contentFingerprint, + SupportsStorageUsage = supportsStorageUsage, }; _guestImages.Add(texture.Address, guestImage); resource.OwnsStorage = false; @@ -8802,6 +9054,12 @@ internal static unsafe class VulkanVideoPresenter return (properties.OptimalTilingFeatures & FormatFeatureFlags.ColorAttachmentBit) != 0; } + private bool SupportsStorageImage(Format format) + { + _vk.GetPhysicalDeviceFormatProperties(_physicalDevice, format, out var properties); + return (properties.OptimalTilingFeatures & FormatFeatureFlags.StorageImageBit) != 0; + } + internal static Format GetTextureFormat(uint format, uint numberType) => (format, numberType) switch { @@ -8882,6 +9140,19 @@ internal static unsafe class VulkanVideoPresenter _ => Format.R8G8B8A8Unorm, }; + internal static Format GetStorageImageFormat(Format format) => + format switch + { + Format.R8Srgb => Format.R8Unorm, + Format.R8G8Srgb => Format.R8G8Unorm, + Format.R8G8B8A8Srgb => Format.R8G8B8A8Unorm, + Format.BC1RgbaSrgbBlock => Format.BC1RgbaUnormBlock, + Format.BC2SrgbBlock => Format.BC2UnormBlock, + Format.BC3SrgbBlock => Format.BC3UnormBlock, + Format.BC7SrgbBlock => Format.BC7UnormBlock, + _ => format, + }; + private static Format GetRenderTargetFormat(uint format, uint numberType) => (format, numberType) switch { @@ -9029,6 +9300,12 @@ internal static unsafe class VulkanVideoPresenter return; } + if (!TryValidateStorageImageBindings(work, out validationError)) + { + LogRejectedComputeDispatch(work, validationError); + return; + } + TranslatedDrawResources? resources = null; CommandBuffer commandBuffer = default; var submitted = false; @@ -9295,6 +9572,59 @@ internal static unsafe class VulkanVideoPresenter return true; } + private bool TryValidateStorageImageBindings( + VulkanComputeGuestDispatch work, + out string error) + { + var storageTextures = work.Textures + .Where(static texture => texture.IsStorage) + .ToArray(); + if (storageTextures.Length == 0) + { + error = string.Empty; + return true; + } + + if (!TryReadSpirvStorageImageContracts( + work.ComputeSpirv, + out var shaderContracts, + out error)) + { + error = $"storage-contract-parse-failed({error})"; + return false; + } + + if (shaderContracts.Length != storageTextures.Length) + { + error = $"storage-binding-count-mismatch(spirv={shaderContracts.Length}," + + $"guest={storageTextures.Length})"; + return false; + } + + for (var index = 0; index < storageTextures.Length; index++) + { + var texture = storageTextures[index]; + var shaderContract = shaderContracts[index]; + var vulkanFormat = GetStorageImageFormat( + GetTextureFormat(texture.Format, texture.NumberType)); + if (!TryValidateStorageImageContract( + shaderContract, + texture.Format, + texture.NumberType, + SupportsStorageImage(vulkanFormat), + out _, + out var bindingError)) + { + error = $"storage-binding[{index}]-invalid(" + + $"addr=0x{texture.Address:X16},reason={bindingError})"; + return false; + } + } + + error = string.Empty; + return true; + } + private void LogRejectedComputeDispatch( VulkanComputeGuestDispatch work, string reason) @@ -9776,24 +10106,23 @@ internal static unsafe class VulkanVideoPresenter return; } - for (var index = 0; index < targetFormats.Length; index++) - { - if (targetFormats[index].IsInteger && - work.Draw.RenderState.Blends[index].Enable) + var normalizedBlends = GuestBlendStateNormalizer.NormalizeIntegerAttachments( + work.Draw.RenderState.Blends, + targetFormats.Select(static format => format.IsInteger).ToArray(), + out var normalizedBlendCount); + var draw = normalizedBlendCount == 0 + ? work.Draw + : work.Draw with { - Console.Error.WriteLine( - "[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment."); - ReturnPooledGuestData(work.Draw); - return; - } - } + RenderState = work.Draw.RenderState with { Blends = normalizedBlends }, + }; if (!_supportsIndependentBlend) { - for (var index = 1; index < work.Draw.RenderState.Blends.Count; index++) + for (var index = 1; index < draw.RenderState.Blends.Count; index++) { - if (work.Draw.RenderState.Blends[index] != - work.Draw.RenderState.Blends[0]) + if (draw.RenderState.Blends[index] != + draw.RenderState.Blends[0]) { Console.Error.WriteLine( "[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend."); @@ -9870,7 +10199,6 @@ internal static unsafe class VulkanVideoPresenter try { var extent = new Extent2D(firstTarget.Width, firstTarget.Height); - var draw = work.Draw; var clearDepthForDraw = draw.RenderState.Depth.ClearEnable; if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable) { @@ -9889,48 +10217,20 @@ internal static unsafe class VulkanVideoPresenter draw.RenderState.Depth) && work.DepthTarget is { } depthTarget) { - var effectiveDepthTarget = depthTarget; - if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height) - { - var matchingTexture = work.Draw.Textures.FirstOrDefault(texture => - texture.Address == depthTarget.Address && - texture.Width >= firstTarget.Width && - texture.Height >= firstTarget.Height); - if (matchingTexture is not null) - { - effectiveDepthTarget = depthTarget with + var resolution = GuestDepthExtentResolver.Resolve( + depthTarget, + firstTarget.Width, + firstTarget.Height, + draw.Textures); + var effectiveDepthTarget = resolution.IsUsable && + (resolution.Width != depthTarget.Width || + resolution.Height != depthTarget.Height) + ? depthTarget with { - Width = matchingTexture.Width, - Height = matchingTexture.Height, - }; - } - else if (depthTarget.Width == 1 && depthTarget.Height == 1) - { - // Some Gen5 streams leave DB_DEPTH_SIZE_XY at its - // clear-state value while binding a full-size DB - // surface. The color attachment and viewport still - // define the render extent; treating that stale 1x1 - // value literally discards the entire draw. - effectiveDepthTarget = depthTarget with - { - Width = firstTarget.Width, - Height = firstTarget.Height, - }; - } - - if (effectiveDepthTarget != depthTarget && - _tracedDepthExtentFallbacks.Add( - (depthTarget.Address, - effectiveDepthTarget.Width, - effectiveDepthTarget.Height))) - { - Console.Error.WriteLine( - $"[LOADER][WARN] Vulkan repaired stale guest depth extent " + - $"addr=0x{depthTarget.Address:X16} " + - $"{depthTarget.Width}x{depthTarget.Height} -> " + - $"{effectiveDepthTarget.Width}x{effectiveDepthTarget.Height}"); - } - } + Width = resolution.Width, + Height = resolution.Height, + } + : depthTarget; depth = GetOrCreateGuestDepth(effectiveDepthTarget); PrepareFirstUseDepth(depth, draw.RenderState.Depth); @@ -10558,8 +10858,17 @@ internal static unsafe class VulkanVideoPresenter private GuestImageResource GetOrCreateGuestImage( GuestRenderTarget target, - Format format) + Format format, + bool requiresStorage = false) { + var supportsStorageUsage = SupportsStorageImage(format); + if (requiresStorage && !supportsStorageUsage) + { + throw new InvalidOperationException( + $"Storage image format {format} is unsupported for guest " + + $"address 0x{target.Address:X16}."); + } + var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels); var guestFormat = GetGuestTextureFormat(target.Format, target.NumberType); var requestedKey = new GuestImageVariantKey( @@ -10577,9 +10886,15 @@ internal static unsafe class VulkanVideoPresenter existing.GuestFormat == guestFormat && existing.Format == format) { + if (requiresStorage && !existing.SupportsStorageUsage) + { + throw new InvalidOperationException( + $"Guest image 0x{target.Address:X16} was created without storage usage."); + } + existing.IsCpuBacked = false; existing.CpuContentFingerprint = 0; - if (existing.RenderPass.Handle == 0) + if (existing.RenderPass.Handle == 0 && !requiresStorage) { var attachmentView = existing.MipViews.Length > 0 ? existing.MipViews[0] @@ -10639,6 +10954,12 @@ internal static unsafe class VulkanVideoPresenter if (_guestImageVariants.Remove(requestedKey, out var retained)) { + if (requiresStorage && !retained.SupportsStorageUsage) + { + throw new InvalidOperationException( + $"Retained guest image 0x{target.Address:X16} was created without storage usage."); + } + retained.IsCpuBacked = false; retained.CpuContentFingerprint = 0; _guestImages.Add(target.Address, retained); @@ -10686,7 +11007,7 @@ internal static unsafe class VulkanVideoPresenter Usage = ImageUsageFlags.ColorAttachmentBit | ImageUsageFlags.SampledBit | - ImageUsageFlags.StorageBit | + (supportsStorageUsage ? ImageUsageFlags.StorageBit : (ImageUsageFlags)0) | ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit, SharingMode = SharingMode.Exclusive, @@ -10742,12 +11063,18 @@ internal static unsafe class VulkanVideoPresenter mipViews[mipLevel] = mipView; } - var (renderPass, initialRenderPass, framebuffer) = - CreateRenderPassAndFramebuffer( - format, - mipViews[0], - target.Width, - target.Height); + RenderPass renderPass = default; + RenderPass initialRenderPass = default; + Framebuffer framebuffer = default; + if (!requiresStorage) + { + (renderPass, initialRenderPass, framebuffer) = + CreateRenderPassAndFramebuffer( + format, + mipViews[0], + target.Width, + target.Height); + } var resource = new GuestImageResource { @@ -10764,6 +11091,7 @@ internal static unsafe class VulkanVideoPresenter RenderPass = renderPass, InitialRenderPass = initialRenderPass, Framebuffer = framebuffer, + SupportsStorageUsage = supportsStorageUsage, }; var debugName = GuestImageDebugName(target, format); SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image"); @@ -10775,9 +11103,12 @@ internal static unsafe class VulkanVideoPresenter mipViews[mipLevel].Handle, $"{debugName} mip{mipLevel}"); } - SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass"); - SetDebugName(ObjectType.RenderPass, initialRenderPass.Handle, $"{debugName} initial-renderpass"); - SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer"); + if (renderPass.Handle != 0) + { + SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass"); + SetDebugName(ObjectType.RenderPass, initialRenderPass.Handle, $"{debugName} initial-renderpass"); + SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer"); + } _guestImages.Add(target.Address, resource); lock (_gate) { @@ -11476,6 +11807,18 @@ internal static unsafe class VulkanVideoPresenter return view; } + private ImageView GetOrCreateGuestImageIdentityView( + GuestImageResource resource, + Format format, + uint mipLevel, + uint levelCount) => + GetOrCreateGuestImageView( + resource, + format, + mipLevel, + levelCount, + dstSelect: 0xFAC); + internal static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat) { if (imageFormat == viewFormat) @@ -14435,6 +14778,10 @@ internal static unsafe class VulkanVideoPresenter } } _descriptorLayouts.Clear(); + while (_recycledDescriptorPools.TryPop(out var recycledDescriptorPool)) + { + _vk.DestroyDescriptorPool(_device, recycledDescriptorPool, null); + } foreach (var sampler in _samplers.Values) { _vk.DestroySampler(_device, sampler, null); @@ -14499,6 +14846,7 @@ internal static unsafe class VulkanVideoPresenter _vk.DestroyInstance(_instance, null); _instance = default; } + GuestDataPool.Trim(); } private void RecreateSwapchainResources(string operation, Result result) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 821eb754..70d95d8d 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -162,6 +162,11 @@ public static partial class Gen5SpirvTranslator return context.TryCompile(out shader, out error); } + internal static SpirvImageFormat DecodeStorageImageFormat( + uint dataFormat, + uint numberType) => + CompilationContext.DecodeStorageImageFormat(dataFormat, numberType); + private sealed partial class CompilationContext { private const uint ImageDescriptorDwords = 8; @@ -279,6 +284,7 @@ public static partial class Gen5SpirvTranslator private uint _workGroupIdInput; private uint _computeDispatchLimit; private uint _pushConstantUintPointer; + private uint _subgroupSizeInput; private uint _subgroupInvocationIdInput; private uint _waveMaskScratch; private uint _waveMaskScratchElementPointer; @@ -961,8 +967,9 @@ public static partial class Gen5SpirvTranslator { var binding = _evaluation.ImageBindings[index]; _imageBindingByPc.TryAdd(binding.Pc, index); - var isStorage = - Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); + var isStorage = Gen5ShaderTranslator.RequiresStorageImage( + binding, + _evaluation.ImageBindings); var (format, componentKind) = DecodeImageFormat(binding.ResourceDescriptor); var componentType = componentKind switch @@ -1044,58 +1051,62 @@ public static partial class Gen5SpirvTranslator return (SpirvImageFormat.Unknown, ImageComponentKind.Float); } - return (dataFormat, numberType) switch + var kind = numberType switch { - (1, _) => (SpirvImageFormat.R8, ImageComponentKind.Float), - (2, _) => (SpirvImageFormat.R16f, ImageComponentKind.Float), - (3, _) => (SpirvImageFormat.Rg8, ImageComponentKind.Float), - (4, 4) => (SpirvImageFormat.R32ui, ImageComponentKind.Uint), - (4, 5) => (SpirvImageFormat.R32i, ImageComponentKind.Sint), - (4, _) => (SpirvImageFormat.R32f, ImageComponentKind.Float), - (5, 4) => (SpirvImageFormat.Rg16ui, ImageComponentKind.Uint), - (5, 5) => (SpirvImageFormat.Rg16i, ImageComponentKind.Sint), - (5, 0) => (SpirvImageFormat.Rg16, ImageComponentKind.Float), - (5, _) => (SpirvImageFormat.Rg16f, ImageComponentKind.Float), - (6 or 7, _) => ( - SpirvImageFormat.R11fG11fB10f, - ImageComponentKind.Float), - (9, 4) => (SpirvImageFormat.Rgb10A2ui, ImageComponentKind.Uint), - (9, _) => (SpirvImageFormat.Rgb10A2, ImageComponentKind.Float), - (10, 4) => (SpirvImageFormat.Rgba8ui, ImageComponentKind.Uint), - (10, 5) => (SpirvImageFormat.Rgba8i, ImageComponentKind.Sint), - (10, _) => (SpirvImageFormat.Rgba8, ImageComponentKind.Float), - (11, 4) => (SpirvImageFormat.Rg32ui, ImageComponentKind.Uint), - (11, 5) => (SpirvImageFormat.Rg32i, ImageComponentKind.Sint), - (11, _) => (SpirvImageFormat.Rg32f, ImageComponentKind.Float), - (12, 4) => (SpirvImageFormat.Rgba16ui, ImageComponentKind.Uint), - (12, 5) => (SpirvImageFormat.Rgba16i, ImageComponentKind.Sint), - (12, 0) => (SpirvImageFormat.Rgba16, ImageComponentKind.Float), - (12, _) => (SpirvImageFormat.Rgba16f, ImageComponentKind.Float), - (13 or 14, 4) => ( - SpirvImageFormat.Rgba32ui, - ImageComponentKind.Uint), - (13 or 14, 5) => ( - SpirvImageFormat.Rgba32i, - ImageComponentKind.Sint), - (13 or 14, _) => ( - SpirvImageFormat.Rgba32f, - ImageComponentKind.Float), - (20, _) => (SpirvImageFormat.R32ui, ImageComponentKind.Uint), - (22, _) => (SpirvImageFormat.Rgba16f, ImageComponentKind.Float), - (29, _) => (SpirvImageFormat.R32f, ImageComponentKind.Float), - (36, _) => (SpirvImageFormat.R8, ImageComponentKind.Float), - (49, _) => (SpirvImageFormat.R8ui, ImageComponentKind.Uint), - (56 or 62 or 64, _) => ( - SpirvImageFormat.Rgba8, - ImageComponentKind.Float), - (71, _) => (SpirvImageFormat.Rgba16f, ImageComponentKind.Float), - (75, _) => (SpirvImageFormat.Rg32f, ImageComponentKind.Float), - (_, 4) => (SpirvImageFormat.Unknown, ImageComponentKind.Uint), - (_, 5) => (SpirvImageFormat.Unknown, ImageComponentKind.Sint), - _ => (SpirvImageFormat.Unknown, ImageComponentKind.Float), + 4 => ImageComponentKind.Uint, + 5 => ImageComponentKind.Sint, + _ => ImageComponentKind.Float, }; + return (DecodeStorageImageFormat(dataFormat, numberType), kind); } + internal static SpirvImageFormat DecodeStorageImageFormat( + uint dataFormat, + uint numberType) => + (dataFormat, numberType) switch + { + (1, 0 or 9) => SpirvImageFormat.R8, + (1, 1) => SpirvImageFormat.R8Snorm, + (1, 4) => SpirvImageFormat.R8ui, + (1, 5) => SpirvImageFormat.R8i, + (2, 0) => SpirvImageFormat.R16, + (2, 1) => SpirvImageFormat.R16Snorm, + (2, 4) => SpirvImageFormat.R16ui, + (2, 5) => SpirvImageFormat.R16i, + (2, 7) => SpirvImageFormat.R16f, + (3, 0 or 9) => SpirvImageFormat.Rg8, + (3, 1) => SpirvImageFormat.Rg8Snorm, + (3, 4) => SpirvImageFormat.Rg8ui, + (3, 5) => SpirvImageFormat.Rg8i, + (4, 4) => SpirvImageFormat.R32ui, + (4, 5) => SpirvImageFormat.R32i, + (4, 7) => SpirvImageFormat.R32f, + (5, 0) => SpirvImageFormat.Rg16, + (5, 1) => SpirvImageFormat.Rg16Snorm, + (5, 4) => SpirvImageFormat.Rg16ui, + (5, 5) => SpirvImageFormat.Rg16i, + (5, 7) => SpirvImageFormat.Rg16f, + (6 or 7, 7) => SpirvImageFormat.R11fG11fB10f, + (8 or 9, 0) => SpirvImageFormat.Rgb10A2, + (8 or 9, 4) => SpirvImageFormat.Rgb10A2ui, + (10, 0 or 9) => SpirvImageFormat.Rgba8, + (10, 1) => SpirvImageFormat.Rgba8Snorm, + (10, 4) => SpirvImageFormat.Rgba8ui, + (10, 5) => SpirvImageFormat.Rgba8i, + (11, 4) => SpirvImageFormat.Rg32ui, + (11, 5) => SpirvImageFormat.Rg32i, + (11, 7) => SpirvImageFormat.Rg32f, + (12, 0) => SpirvImageFormat.Rgba16, + (12, 1) => SpirvImageFormat.Rgba16Snorm, + (12, 4) => SpirvImageFormat.Rgba16ui, + (12, 5) => SpirvImageFormat.Rgba16i, + (12, 7) => SpirvImageFormat.Rgba16f, + (13 or 14, 4) => SpirvImageFormat.Rgba32ui, + (13 or 14, 5) => SpirvImageFormat.Rgba32i, + (13 or 14, 7) => SpirvImageFormat.Rgba32f, + _ => SpirvImageFormat.Unknown, + }; + private void DeclareStageInterface() { if (UsesSubgroupOperations()) @@ -1111,6 +1122,18 @@ public static partial class Gen5SpirvTranslator (uint)SpirvBuiltIn.SubgroupLocalInvocationId); _interfaces.Add(_subgroupInvocationIdInput); + if (_emulateWave64) + { + _subgroupSizeInput = _module.AddGlobalVariable( + subgroupPointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _subgroupSizeInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.SubgroupSize); + _interfaces.Add(_subgroupSizeInput); + } + if (_waveLaneCount == 64) { _localInvocationIndexInput = _module.AddGlobalVariable( @@ -3091,12 +3114,14 @@ public static partial class Gen5SpirvTranslator return true; } + var imageLoad = Gen5ShaderTranslator.IsImageLoadOperation(instruction.Opcode); var storage = Gen5ShaderTranslator.IsStorageImageOperation(instruction.Opcode); for (var index = 0; index < _evaluation.ImageBindings.Count; index++) { var candidate = _evaluation.ImageBindings[index]; if (candidate.Control.ScalarResource != control.ScalarResource || candidate.Control.ScalarSampler != control.ScalarSampler || + Gen5ShaderTranslator.IsImageLoadOperation(candidate.Opcode) != imageLoad || Gen5ShaderTranslator.IsStorageImageOperation(candidate.Opcode) != storage || !HasSameScalarDefinitions( candidate.Pc, @@ -5097,12 +5122,14 @@ public static partial class Gen5SpirvTranslator SpirvOp.UConvert, _ulongType, maskedLane)); - return _module.AddInstruction( - SpirvOp.Select, - _ulongType, - IsCurrentLaneInRdnaWave(), - shifted, - _module.Constant64(_ulongType, 0)); + return _emulateWave64 + ? shifted + : _module.AddInstruction( + SpirvOp.Select, + _ulongType, + IsCurrentLaneInRdnaWave(), + shifted, + _module.Constant64(_ulongType, 0)); } private uint IsCurrentLaneInRdnaWave() => @@ -5139,6 +5166,11 @@ public static partial class Gen5SpirvTranslator 0); if (_emulateWave64) { + var high = _module.AddInstruction( + SpirvOp.CompositeExtract, + _uintType, + ballot, + 1); var subgroupLane = Load(_uintType, _subgroupInvocationIdInput); var firstLane = _module.AddInstruction( @@ -5150,6 +5182,16 @@ public static partial class Gen5SpirvTranslator EmitConditional(firstLane, () => { Store(WaveMaskScratchPointer(half), low); + + var nativeWave64 = _module.AddInstruction( + SpirvOp.UGreaterThanEqual, + _boolType, + Load(_uintType, _subgroupSizeInput), + UInt(64)); + EmitConditional(nativeWave64, () => + { + Store(WaveMaskScratchPointer(UInt(1)), high); + }); }); EmitWave64Barrier(); var lowMask = Load( diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs index 52ff5edc..62c8efc3 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/SpirvModuleBuilder.cs @@ -251,6 +251,7 @@ public enum SpirvBuiltIn : uint LocalInvocationId = 27, GlobalInvocationId = 28, LocalInvocationIndex = 29, + SubgroupSize = 36, SubgroupLocalInvocationId = 41, } diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index a6347b2b..50426fc4 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -1576,11 +1576,37 @@ public static class Gen5ShaderTranslator private static bool IsMimgInstruction(string name) => name.StartsWith("Image", StringComparison.Ordinal); + public static bool IsImageLoadOperation(string name) => + name.StartsWith("ImageLoad", StringComparison.Ordinal); + public static bool IsStorageImageOperation(string name) => - name.StartsWith("ImageLoad", StringComparison.Ordinal) || name.StartsWith("ImageStore", StringComparison.Ordinal) || name.StartsWith("ImageAtomic", StringComparison.Ordinal); + public static bool RequiresStorageImage( + Gen5ImageBinding binding, + IReadOnlyList stageBindings) + { + if (IsStorageImageOperation(binding.Opcode)) + { + return true; + } + + if (!IsImageLoadOperation(binding.Opcode)) + { + return false; + } + + // IMAGE_LOAD itself is read-only and maps naturally to OpImageFetch, + // including for block-compressed textures which Vulkan cannot expose + // as storage images. Keep it as storage only when the same resolved + // descriptor is also written in this shader stage, preserving coherent + // read/write access through one storage-image representation. + return stageBindings.Any(candidate => + IsStorageImageOperation(candidate.Opcode) && + binding.ResourceDescriptor.SequenceEqual(candidate.ResourceDescriptor)); + } + public static bool IsDataShareAtomic(string name) => name switch { "DsAddU32" or "DsSubU32" or "DsIncU32" or "DsDecU32" or