diff --git a/SharpEmu.slnx b/SharpEmu.slnx index aa18593..a59b488 100644 --- a/SharpEmu.slnx +++ b/SharpEmu.slnx @@ -21,6 +21,7 @@ SPDX-License-Identifier: GPL-2.0-or-later + diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 62257b6..0715c74 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -8328,16 +8328,33 @@ public static partial class AgcExports return tailLinear; } + var volumeDepth = checked((int)GetTextureVolumeDepth( + descriptor.Type, + descriptor.Depth)); + if (logicalByteCount % volumeDepth != 0 || + source.Length % volumeDepth != 0) + { + return null; + } + + var logicalSliceByteCount = logicalByteCount / volumeDepth; + var physicalSliceByteCount = source.Length / volumeDepth; var linear = new byte[logicalByteCount]; - return GnmTiling.TryDetile( - source, - linear, - descriptor.TileMode, - elementsWide, - elementsHigh, - bytesPerElement) - ? linear - : null; + for (var slice = 0; slice < volumeDepth; slice++) + { + if (!GnmTiling.TryDetile( + source.AsSpan(slice * physicalSliceByteCount, physicalSliceByteCount), + linear.AsSpan(slice * logicalSliceByteCount, logicalSliceByteCount), + descriptor.TileMode, + elementsWide, + elementsHigh, + bytesPerElement)) + { + return null; + } + } + + return linear; } private static void TraceTextureFallback(TextureDescriptor descriptor, string reason) @@ -8369,6 +8386,9 @@ public static partial class AgcExports out GuestDrawTexture texture) { texture = default!; + var textureDepth = GetTextureVolumeDepth( + descriptor.Type, + descriptor.Depth); if ((descriptor.Type != Gen5TextureType1D && descriptor.Type != Gen5TextureType2D && descriptor.Type != Gen5TextureType3D && @@ -8381,7 +8401,13 @@ public static partial class AgcExports descriptor.Height > 8192) { TraceTextureFallback(descriptor, "invalid-descriptor"); - texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed); + texture = CreateFallbackGuestDrawTexture( + isStorage, + descriptor.Format, + descriptor.NumberType, + isArrayed, + descriptor.Type, + textureDepth); return true; } @@ -8391,10 +8417,15 @@ public static partial class AgcExports descriptor.Height, descriptor.Format) : descriptor.Width; - var sourceByteCount = GetTextureByteCount( + var sourceSliceByteCount = GetTextureByteCount( descriptor.Format, sourceWidth, descriptor.Height); + var sourceByteCount = GetTextureByteCount( + descriptor.Format, + sourceWidth, + descriptor.Height, + textureDepth); if (sourceByteCount == 0 || sourceByteCount > MaxPresentedTextureBytes || sourceByteCount > int.MaxValue) @@ -8402,11 +8433,17 @@ public static partial class AgcExports TraceTextureFallback( descriptor, $"invalid-byte-count:{sourceByteCount}"); - texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed); + texture = CreateFallbackGuestDrawTexture( + isStorage, + descriptor.Format, + descriptor.NumberType, + isArrayed, + descriptor.Type, + textureDepth); return true; } - var physicalSourceByteCount = sourceByteCount; + var physicalSourceByteCount = sourceSliceByteCount; var elementsWide = 0; var elementsHigh = 0; var bytesPerElement = 0; @@ -8428,13 +8465,6 @@ public static partial class AgcExports physicalSourceByteCount = tiledByteCount; } - if (physicalSourceByteCount > MaxPresentedTextureBytes || - physicalSourceByteCount > int.MaxValue) - { - texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed); - return true; - } - var resourceMipLevels = descriptor.HasExtendedDescriptor ? descriptor.ResourceMipLevels : 1u; @@ -8459,6 +8489,20 @@ public static partial class AgcExports chainSliceBytes = placedChainSliceBytes; } + physicalSourceByteCount = checked(physicalSourceByteCount * textureDepth); + if (physicalSourceByteCount > MaxPresentedTextureBytes || + physicalSourceByteCount > int.MaxValue) + { + texture = CreateFallbackGuestDrawTexture( + isStorage, + descriptor.Format, + descriptor.NumberType, + isArrayed, + descriptor.Type, + textureDepth); + return true; + } + var wantsArrayUpload = isArrayed && !isStorage && descriptor.Address != 0 && @@ -8497,7 +8541,9 @@ public static partial class AgcExports TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, Sampler: ToGuestSampler(samplerDescriptor), - ArrayedView: isArrayed); + ArrayedView: isArrayed, + Type: descriptor.Type, + Depth: textureDepth); return true; } @@ -8573,7 +8619,9 @@ public static partial class AgcExports Pitch: sourceWidth, TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, - Sampler: ToGuestSampler(samplerDescriptor)); + Sampler: ToGuestSampler(samplerDescriptor), + Type: descriptor.Type, + Depth: textureDepth); return true; } @@ -8614,7 +8662,9 @@ public static partial class AgcExports sourceWidth, sampler, isArrayed, - arrayUploadLayers))) + arrayUploadLayers, + descriptor.Type, + textureDepth))) { texture = new GuestDrawTexture( descriptor.Address, @@ -8634,14 +8684,16 @@ public static partial class AgcExports DstSelect: descriptor.DstSelect, Sampler: sampler, ArrayedView: isArrayed, - ArrayLayers: arrayUploadLayers); + ArrayLayers: arrayUploadLayers, + Type: descriptor.Type, + Depth: textureDepth); return true; } if (wantsArrayUpload) { var arrayLayers = arrayUploadLayers; - var layerBytes = checked((int)sourceByteCount); + var layerBytes = checked((int)sourceSliceByteCount); var totalBytes = (long)layerBytes * arrayLayers; if (totalBytes <= int.MaxValue) { @@ -8649,7 +8701,7 @@ public static partial class AgcExports var uploadedLayers = 0u; for (var layer = 0u; layer < arrayLayers; layer++) { - var sliceSource = new byte[(int)physicalSourceByteCount]; + var sliceSource = new byte[(int)chainSliceBytes]; if (!ctx.Memory.TryRead( descriptor.Address + layer * chainSliceBytes + baseMipByteOffset, sliceSource)) @@ -8690,7 +8742,9 @@ public static partial class AgcExports DstSelect: descriptor.DstSelect, Sampler: sampler, ArrayedView: true, - ArrayLayers: arrayLayers); + ArrayLayers: arrayLayers, + Type: descriptor.Type, + Depth: textureDepth); return true; } } @@ -8704,7 +8758,13 @@ public static partial class AgcExports TraceTextureFallback( descriptor, $"guest-read-failed:{sourceByteCount}"); - texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType, isArrayed); + texture = CreateFallbackGuestDrawTexture( + isStorage, + descriptor.Format, + descriptor.NumberType, + isArrayed, + descriptor.Type, + textureDepth); return true; } @@ -8759,7 +8819,9 @@ public static partial class AgcExports DstSelect: descriptor.DstSelect, Sampler: ToGuestSampler(samplerDescriptor), WriteGeneration: hasWriteGeneration ? writeGeneration : -1, - ArrayedView: isArrayed); + ArrayedView: isArrayed, + Type: descriptor.Type, + Depth: textureDepth); return true; } @@ -9039,7 +9101,9 @@ public static partial class AgcExports bool isStorage, uint format, uint numberType, - bool isArrayed = false) + bool isArrayed = false, + uint type = Gen5TextureType2D, + uint depth = 1) { var fallbackFormat = format == 0 ? 10u : format; var fallbackNumberType = numberType; @@ -9054,7 +9118,9 @@ public static partial class AgcExports IsStorage: isStorage, MipLevels: 1, MipLevel: 0, - ArrayedView: isArrayed); + ArrayedView: isArrayed, + Type: type, + Depth: GetTextureVolumeDepth(type, depth)); } private static GuestSampler ToGuestSampler(IReadOnlyList descriptor) => @@ -10240,7 +10306,8 @@ public static partial class AgcExports var totalBytes = GetTextureByteCount( texture.Format, texture.Width, - texture.Height); + texture.Height, + GetTextureVolumeDepth(texture.Type, texture.Depth)); if (totalBytes == 0) { return "probe=unsupported"; @@ -10323,20 +10390,37 @@ public static partial class AgcExports _ => 0UL, }; - private static ulong GetTextureByteCount(uint format, uint width, uint height) + internal static ulong GetTextureByteCount( + uint format, + uint width, + uint height, + uint depth = 1) { var bytesPerTexel = GetTextureBytesPerTexel(format); if (bytesPerTexel != 0) { - return checked((ulong)width * height * bytesPerTexel); + return checked( + (ulong)width * + height * + Math.Max(depth, 1u) * + bytesPerTexel); } var blockBytes = (ulong)GetBlockCompressedBlockBytes(format); return blockBytes == 0 ? 0 - : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); + : checked( + ((ulong)width + 3) / 4 * + (((ulong)height + 3) / 4) * + Math.Max(depth, 1u) * + blockBytes); } + internal static uint GetTextureVolumeDepth(uint type, uint depth) => + type == Gen5TextureType3D + ? Math.Max(depth, 1u) + : 1u; + private static uint GetLinearTexturePitch(uint pitch, uint height, uint format) { var bytesPerTexel = GetTextureBytesPerTexel(format); diff --git a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs index 91a2ea3..6b4c7f8 100644 --- a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs +++ b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs @@ -10,7 +10,8 @@ namespace SharpEmu.Libs.Gpu; // translation for its API. /// A guest texture referenced by a draw or dispatch. Format/NumberType/ -/// TileMode/DstSelect are raw guest descriptor codes. +/// TileMode/DstSelect/Type are raw guest descriptor codes. Depth is the +/// normalized volume depth (one for non-3D resources). internal sealed record GuestDrawTexture( ulong Address, uint Width, @@ -32,7 +33,9 @@ internal sealed record GuestDrawTexture( // from; -1 when the range is untracked or the pixels were not read here. long WriteGeneration = -1, bool ArrayedView = false, - uint ArrayLayers = 1); + uint ArrayLayers = 1, + uint Type = 9, + uint Depth = 1); /// Raw guest sampler descriptor dwords, copied verbatim from guest memory. internal readonly record struct GuestSampler( @@ -55,7 +58,9 @@ internal readonly record struct TextureContentIdentity( uint Pitch, GuestSampler Sampler, bool Arrayed = false, - uint ArrayLayers = 1); + uint ArrayLayers = 1, + uint Type = 9, + uint Depth = 1); internal sealed record GuestMemoryBuffer( ulong BaseAddress, diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index e2ecd73..e6a9088 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -113,6 +113,26 @@ 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 const uint Gen5TextureType2D = 9; + internal const uint Gen5TextureType3D = 10; + + internal static bool IsGuestTexture3D(uint type) => + type == Gen5TextureType3D; + + internal static uint GetGuestTextureDepth(uint type, uint depth) => + IsGuestTexture3D(type) ? Math.Max(depth, 1u) : 1u; + + internal static ImageType GetGuestTextureImageType(uint type) => + IsGuestTexture3D(type) ? ImageType.Type3D : ImageType.Type2D; + + internal static ImageViewType GetGuestTextureViewType( + uint type, + bool arrayedView = false) => + IsGuestTexture3D(type) + ? ImageViewType.Type3D + : arrayedView + ? ImageViewType.Type2DArray + : ImageViewType.Type2D; internal enum StorageImageComponentKind { @@ -1759,6 +1779,13 @@ internal static unsafe class VulkanVideoPresenter return checked((ulong)width * height * bytesPerPixel); } + internal static ulong GetGuestImageByteCount( + uint format, + uint width, + uint height, + uint depth) => + checked(GetGuestImageByteCount(format, width, height) * Math.Max(depth, 1u)); + // Maps a UNORM swapchain format to the sRGB view of the same bit layout, // or Undefined when no counterpart exists. Used to encode linear-float // guest flips on their way into a UNORM swapchain. @@ -2957,6 +2984,8 @@ internal static unsafe class VulkanVideoPresenter ulong Address, uint Width, uint Height, + uint Depth, + uint Type, uint MipLevels, uint GuestFormat, Format Format); @@ -3110,9 +3139,12 @@ internal static unsafe class VulkanVideoPresenter public ImageView View; public uint Width; public uint Height; + public uint Depth = 1; + public uint Type = Gen5TextureType2D; public uint RowLength; public uint DstSelect; public uint Layers = 1; + public uint MipLevel; public bool NeedsUpload; public bool OwnsStorage; public bool IsStorage; @@ -3214,9 +3246,12 @@ internal static unsafe class VulkanVideoPresenter public long FlipVersion; public uint Width; public uint Height; + public uint Depth = 1; + public uint Type = Gen5TextureType2D; // Unscaled guest-requested size; Width/Height are the physical (scaled) backing size. public uint LogicalWidth; public uint LogicalHeight; + public uint LogicalDepth = 1; public uint MipLevels; public uint GuestFormat; public Format Format; @@ -5599,7 +5634,8 @@ internal static unsafe class VulkanVideoPresenter var rowLength = texture.TileMode == 0 ? Math.Max(texture.Pitch, width) : width; - var byteCount = GetTextureByteCount(texture.Format, rowLength, height); + var depth = GetGuestTextureDepth(texture.Type, texture.Depth); + var byteCount = GetTextureByteCount(texture.Format, rowLength, height, depth); if (byteCount == 0 || byteCount > int.MaxValue) { return null; @@ -7461,6 +7497,8 @@ internal static unsafe class VulkanVideoPresenter View = view, Width = guestImage.Width, Height = guestImage.Height, + Depth = guestImage.Depth, + Type = guestImage.Type, RowLength = guestImage.Width, DstSelect = texture.DstSelect, SamplerState = texture.Sampler, @@ -7500,6 +7538,8 @@ internal static unsafe class VulkanVideoPresenter if (!guestImage.IsCpuBacked || guestImage.Width != texture.Width || guestImage.Height != texture.Height || + guestImage.Depth != GetGuestTextureDepth(texture.Type, texture.Depth) || + IsGuestTexture3D(guestImage.Type) != IsGuestTexture3D(texture.Type) || guestImage.MipLevels != 1 || texture.RgbaPixels.Length == 0) { @@ -7509,7 +7549,12 @@ internal static unsafe class VulkanVideoPresenter var rowLength = texture.TileMode == 0 ? Math.Max(texture.Pitch, texture.Width) : texture.Width; - var expectedSize = GetTextureByteCount(texture.Format, rowLength, texture.Height); + var depth = GetGuestTextureDepth(texture.Type, texture.Depth); + var expectedSize = GetTextureByteCount( + texture.Format, + rowLength, + texture.Height, + depth); if (expectedSize == 0 || expectedSize > int.MaxValue) { return false; @@ -7556,6 +7601,8 @@ internal static unsafe class VulkanVideoPresenter View = view, Width = guestImage.Width, Height = guestImage.Height, + Depth = guestImage.Depth, + Type = guestImage.Type, RowLength = rowLength, DstSelect = texture.DstSelect, NeedsUpload = true, @@ -7665,6 +7712,12 @@ internal static unsafe class VulkanVideoPresenter GuestDrawTexture texture, out TextureResource resource) { + if (IsGuestTexture3D(texture.Type)) + { + resource = null!; + return false; + } + foreach (var depth in _guestDepthImages.Values) { if (texture.Address != depth.Address && @@ -7761,7 +7814,9 @@ internal static unsafe class VulkanVideoPresenter texture.Pitch, texture.Sampler, texture.ArrayedView, - Math.Max(texture.ArrayLayers, 1)); + Math.Max(texture.ArrayLayers, 1), + Type: texture.Type, + Depth: GetGuestTextureDepth(texture.Type, texture.Depth)); if (_textureCache.TryGetValue(key, out var cached)) { return cached; @@ -7886,6 +7941,20 @@ internal static unsafe class VulkanVideoPresenter GuestDrawTexture texture, GuestImageResource guestImage) { + var textureIs3D = IsGuestTexture3D(texture.Type); + if (textureIs3D != IsGuestTexture3D(guestImage.Type)) + { + return false; + } + + if (textureIs3D) + { + return texture.Width == guestImage.LogicalWidth && + texture.Height == guestImage.LogicalHeight && + GetGuestTextureDepth(texture.Type, texture.Depth) == + guestImage.LogicalDepth; + } + if (guestImage.LogicalWidth == texture.Width && guestImage.LogicalHeight == texture.Height) { @@ -7921,6 +7990,9 @@ internal static unsafe class VulkanVideoPresenter $"format={texture.Format}/num={texture.NumberType}."); } var selectedMipLevel = GetStorageMipLevel(texture); + var mipWidth = GetMipDimension(guestImage.Width, selectedMipLevel); + var mipHeight = GetMipDimension(guestImage.Height, selectedMipLevel); + var mipDepth = GetMipDimension(guestImage.Depth, selectedMipLevel); var view = GetOrCreateGuestImageIdentityView( guestImage, vkFormat, @@ -7931,10 +8003,13 @@ internal static unsafe class VulkanVideoPresenter Address = texture.Address, Image = guestImage.Image, View = view, - Width = guestImage.Width, - Height = guestImage.Height, - RowLength = guestImage.Width, + Width = mipWidth, + Height = mipHeight, + Depth = mipDepth, + Type = guestImage.Type, + RowLength = mipWidth, DstSelect = texture.DstSelect, + MipLevel = selectedMipLevel, IsStorage = true, SamplerState = texture.Sampler, GuestImage = guestImage, @@ -7947,7 +8022,8 @@ internal static unsafe class VulkanVideoPresenter var expectedSize = GetTextureByteCount( texture.Format, texture.Width, - texture.Height); + texture.Height, + GetGuestTextureDepth(texture.Type, texture.Depth)); if ((ulong)texture.RgbaPixels.Length == expectedSize && texture.RgbaPixels.AsSpan().IndexOfAnyExcept((byte)0) >= 0) { @@ -7998,6 +8074,7 @@ internal static unsafe class VulkanVideoPresenter { var width = Math.Max(texture.Width, 1); var height = Math.Max(texture.Height, 1); + var depth = GetGuestTextureDepth(texture.Type, texture.Depth); var vkFormat = GetStorageImageFormat( GetTextureFormat(texture.Format, texture.NumberType)); if (!SupportsStorageImage(vkFormat)) @@ -8009,9 +8086,9 @@ internal static unsafe class VulkanVideoPresenter var imageInfo = new ImageCreateInfo { SType = StructureType.ImageCreateInfo, - ImageType = ImageType.Type2D, + ImageType = GetGuestTextureImageType(texture.Type), Format = vkFormat, - Extent = new Extent3D(width, height, 1), + Extent = new Extent3D(width, height, depth), MipLevels = 1, ArrayLayers = 1, Samples = SampleCountFlags.Count1Bit, @@ -8047,7 +8124,7 @@ internal static unsafe class VulkanVideoPresenter { SType = StructureType.ImageViewCreateInfo, Image = image, - ViewType = ImageViewType.Type2D, + ViewType = GetGuestTextureViewType(texture.Type), Format = vkFormat, Components = new ComponentMapping( ComponentSwizzle.Identity, @@ -8067,8 +8144,11 @@ internal static unsafe class VulkanVideoPresenter Address = 0, Width = width, Height = height, + Depth = depth, + Type = texture.Type, LogicalWidth = width, LogicalHeight = height, + LogicalDepth = depth, MipLevels = 1, GuestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType), Format = vkFormat, @@ -8086,6 +8166,8 @@ internal static unsafe class VulkanVideoPresenter View = view, Width = width, Height = height, + Depth = depth, + Type = texture.Type, RowLength = width, DstSelect = texture.DstSelect, OwnsStorage = true, @@ -8113,7 +8195,9 @@ internal static unsafe class VulkanVideoPresenter texture.NumberType, texture.ResourceMipLevels), format, - requiresStorage: true); + requiresStorage: true, + texture.Type, + GetGuestTextureDepth(texture.Type, texture.Depth)); var selectedMipLevel = GetStorageMipLevel(texture); if (selectedMipLevel >= guestImage.MipLevels) { @@ -8146,21 +8230,29 @@ internal static unsafe class VulkanVideoPresenter { var width = Math.Max(texture.Width, 1); var height = Math.Max(texture.Height, 1); + var depth = GetGuestTextureDepth(texture.Type, texture.Depth); var rowLength = texture.TileMode == 0 ? Math.Max(texture.Pitch, width) : width; var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); - var layers = Math.Max(texture.ArrayLayers, 1); - var expectedSize = GetTextureByteCount(texture.Format, rowLength, height); + var layers = IsGuestTexture3D(texture.Type) + ? 1u + : Math.Max(texture.ArrayLayers, 1); + var expectedSize = GetTextureByteCount( + texture.Format, + rowLength, + height, + depth); if (ShouldTraceVulkanResources() && _tracedTextureUploads.Add((texture.Address, width, height, vkFormat))) { Console.Error.WriteLine( $"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " + $"fmt={texture.Format} num={texture.NumberType} vk={vkFormat} " + - $"size={width}x{height} row={rowLength} tile={texture.TileMode} " + - $"layers={layers} dst=0x{texture.DstSelect:X3} " + + $"size={width}x{height}x{depth} type={texture.Type} " + + $"row={rowLength} tile={texture.TileMode} layers={layers} " + + $"dst=0x{texture.DstSelect:X3} " + $"bytes={texture.RgbaPixels.Length} expected={expectedSize}"); } var pixels = texture.RgbaPixels.Length == (int)(expectedSize * layers) @@ -8189,26 +8281,32 @@ internal static unsafe class VulkanVideoPresenter uploadPixels, $"{TextureDebugName(texture, vkFormat)} staging"); - var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat); - var supportsStorageUsage = supportsAttachmentUsage && + var supportsMutableUsage = !IsBlockCompressedFormat(vkFormat); + var supportsAttachmentUsage = + supportsMutableUsage && + !IsGuestTexture3D(texture.Type); + var supportsStorageUsage = + supportsMutableUsage && SupportsStorageImage(vkFormat); var imageInfo = new ImageCreateInfo { SType = StructureType.ImageCreateInfo, - Flags = supportsAttachmentUsage + Flags = supportsMutableUsage ? ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit : 0, - ImageType = ImageType.Type2D, + ImageType = GetGuestTextureImageType(texture.Type), Format = vkFormat, - Extent = new Extent3D(width, height, 1), + Extent = new Extent3D(width, height, depth), MipLevels = 1, ArrayLayers = layers, Samples = SampleCountFlags.Count1Bit, Tiling = ImageTiling.Optimal, - Usage = supportsAttachmentUsage + Usage = supportsMutableUsage ? ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit | - ImageUsageFlags.ColorAttachmentBit | + (supportsAttachmentUsage + ? ImageUsageFlags.ColorAttachmentBit + : (ImageUsageFlags)0) | (supportsStorageUsage ? ImageUsageFlags.StorageBit : (ImageUsageFlags)0) | ImageUsageFlags.TransferSrcBit : ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit, @@ -8232,9 +8330,9 @@ internal static unsafe class VulkanVideoPresenter { SType = StructureType.ImageViewCreateInfo, Image = image, - ViewType = texture.ArrayedView - ? ImageViewType.Type2DArray - : ImageViewType.Type2D, + ViewType = GetGuestTextureViewType( + texture.Type, + texture.ArrayedView), Format = vkFormat, Components = ToVkComponentMapping(texture.DstSelect), SubresourceRange = ColorSubresourceRange(layerCount: layers), @@ -8253,6 +8351,8 @@ internal static unsafe class VulkanVideoPresenter View = view, Width = width, Height = height, + Depth = depth, + Type = texture.Type, RowLength = rowLength, DstSelect = texture.DstSelect, Layers = layers, @@ -8275,8 +8375,11 @@ internal static unsafe class VulkanVideoPresenter Address = texture.Address, Width = width, Height = height, + Depth = depth, + Type = texture.Type, LogicalWidth = width, LogicalHeight = height, + LogicalDepth = depth, MipLevels = 1, GuestFormat = guestFormat, Format = vkFormat, @@ -9731,7 +9834,13 @@ internal static unsafe class VulkanVideoPresenter { if (format is 9 or 10) { - return CreateBlackFrame(width, height); + var pixels = new byte[checked((int)expectedSize)]; + for (var offset = 3; offset < pixels.Length; offset += 4) + { + pixels[offset] = 0xFF; + } + + return pixels; } return new byte[checked((int)expectedSize)]; @@ -9762,6 +9871,13 @@ internal static unsafe class VulkanVideoPresenter private static ulong GetTextureByteCount(uint format, uint width, uint height) => GetGuestImageByteCount(format, width, height); + private static ulong GetTextureByteCount( + uint format, + uint width, + uint height, + uint depth) => + GetGuestImageByteCount(format, width, height, depth); + private static ulong GetVulkanImageByteCount(Format format, uint width, uint height) { var blockBytes = format switch @@ -9808,6 +9924,13 @@ internal static unsafe class VulkanVideoPresenter : checked((ulong)width * height * bitsPerTexel / 8); } + private static ulong GetVulkanImageByteCount( + Format format, + uint width, + uint height, + uint depth) => + checked(GetVulkanImageByteCount(format, width, height) * Math.Max(depth, 1u)); + private bool SupportsColorAttachment(Format format) { _vk.GetPhysicalDeviceFormatProperties(_physicalDevice, format, out var properties); @@ -11786,7 +11909,8 @@ internal static unsafe class VulkanVideoPresenter return 0; } - var texelCount = (ulong)target.Width * target.Height; + var rowsPerVolume = checked((ulong)target.Height * target.Depth); + var texelCount = checked((ulong)target.Width * rowsPerVolume); if (texelCount == 0 || expectedByteCount % texelCount != 0) { // Block-compressed or otherwise non-linear: no per-texel pitch. @@ -11794,12 +11918,12 @@ internal static unsafe class VulkanVideoPresenter } var bytesPerTexel = expectedByteCount / texelCount; - if (bytesPerTexel == 0 || uploadByteCount % target.Height != 0) + if (bytesPerTexel == 0 || uploadByteCount % rowsPerVolume != 0) { return 0; } - var rowBytes = uploadByteCount / target.Height; + var rowBytes = uploadByteCount / rowsPerVolume; if (rowBytes % bytesPerTexel != 0) { return 0; @@ -11836,7 +11960,8 @@ internal static unsafe class VulkanVideoPresenter var expectedByteCount = GetVulkanImageByteCount( target.Format, target.Width, - target.Height); + target.Height, + target.Depth); // The guest can hand us linear pixel data whose rows are padded out // to a hardware pitch wider than the image, so the byte count runs @@ -11931,7 +12056,10 @@ internal static unsafe class VulkanVideoPresenter 0, 1), ImageOffset = default, - ImageExtent = new Extent3D(target.Width, target.Height, 1), + ImageExtent = new Extent3D( + target.Width, + target.Height, + target.Depth), }; _vk.CmdCopyBufferToImage( commandBuffer, @@ -11993,8 +12121,11 @@ internal static unsafe class VulkanVideoPresenter private GuestImageResource GetOrCreateGuestImage( GuestRenderTarget target, Format format, - bool requiresStorage = false) + bool requiresStorage = false, + uint type = Gen5TextureType2D, + uint depth = 1) { + depth = GetGuestTextureDepth(type, depth); var supportsStorageUsage = SupportsStorageImage(format); if (requiresStorage && !supportsStorageUsage) { @@ -12010,12 +12141,19 @@ internal static unsafe class VulkanVideoPresenter var physicalHeight = requiresStorage ? target.Height : ScaleGuestDimension(target.Height); - var mipLevels = ClampMipLevels(physicalWidth, physicalHeight, target.MipLevels); + var physicalDepth = depth; + var mipLevels = ClampMipLevels( + physicalWidth, + physicalHeight, + physicalDepth, + target.MipLevels); var guestFormat = GetGuestTextureFormat(target.Format, target.NumberType); var requestedKey = new GuestImageVariantKey( target.Address, target.Width, target.Height, + depth, + type, mipLevels, guestFormat, format); @@ -12023,6 +12161,8 @@ internal static unsafe class VulkanVideoPresenter { if (existing.LogicalWidth == target.Width && existing.LogicalHeight == target.Height && + existing.LogicalDepth == depth && + existing.Type == type && existing.MipLevels == mipLevels && existing.GuestFormat == guestFormat && existing.Format == format) @@ -12035,7 +12175,9 @@ internal static unsafe class VulkanVideoPresenter existing.IsCpuBacked = false; existing.CpuContentFingerprint = 0; - if (existing.RenderPass.Handle == 0 && !requiresStorage) + if (existing.RenderPass.Handle == 0 && + !requiresStorage && + !IsGuestTexture3D(type)) { var attachmentView = existing.MipViews.Length > 0 ? existing.MipViews[0] @@ -12074,9 +12216,11 @@ internal static unsafe class VulkanVideoPresenter existing.Address, existing.LogicalWidth, existing.LogicalHeight, + existing.LogicalDepth, + existing.Type, existing.MipLevels, - existing.GuestFormat, - existing.Format), + existing.GuestFormat, + existing.Format), existing); _guestImages.Remove(target.Address); lock (_gate) @@ -12111,14 +12255,19 @@ internal static unsafe class VulkanVideoPresenter _guestImageExtents[target.Address] = ( target.Width, target.Height, - GetTextureByteCount(target.Format, target.Width, target.Height)); + GetTextureByteCount( + target.Format, + target.Width, + target.Height, + depth)); } if (target.Width <= 1920 && target.Height <= 1080) { SharpEmu.HLE.GuestImageWriteTracker.Track( target.Address, - (ulong)target.Width * target.Height * GetTextureBytesPerPixel(target.Format), + (ulong)target.Width * target.Height * depth * + GetTextureBytesPerPixel(target.Format), CurrentGuestWorkSequenceForDiagnostics, "vulkan.render-target"); } @@ -12140,15 +12289,17 @@ internal static unsafe class VulkanVideoPresenter Flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit, - ImageType = ImageType.Type2D, + ImageType = GetGuestTextureImageType(type), Format = format, - Extent = new Extent3D(physicalWidth, physicalHeight, 1), + Extent = new Extent3D(physicalWidth, physicalHeight, physicalDepth), MipLevels = mipLevels, ArrayLayers = 1, Samples = SampleCountFlags.Count1Bit, Tiling = ImageTiling.Optimal, Usage = - ImageUsageFlags.ColorAttachmentBit | + (IsGuestTexture3D(type) + ? (ImageUsageFlags)0 + : ImageUsageFlags.ColorAttachmentBit) | ImageUsageFlags.SampledBit | (supportsStorageUsage ? ImageUsageFlags.StorageBit : (ImageUsageFlags)0) | ImageUsageFlags.TransferSrcBit | @@ -12178,7 +12329,7 @@ internal static unsafe class VulkanVideoPresenter { SType = StructureType.ImageViewCreateInfo, Image = image, - ViewType = ImageViewType.Type2D, + ViewType = GetGuestTextureViewType(type), Format = format, Components = new ComponentMapping( ComponentSwizzle.Identity, @@ -12209,7 +12360,7 @@ internal static unsafe class VulkanVideoPresenter RenderPass renderPass = default; RenderPass initialRenderPass = default; Framebuffer framebuffer = default; - if (!requiresStorage) + if (!requiresStorage && !IsGuestTexture3D(type)) { (renderPass, initialRenderPass, framebuffer) = CreateRenderPassAndFramebuffer( @@ -12224,8 +12375,11 @@ internal static unsafe class VulkanVideoPresenter Address = target.Address, Width = physicalWidth, Height = physicalHeight, + Depth = physicalDepth, + Type = type, LogicalWidth = target.Width, LogicalHeight = target.Height, + LogicalDepth = depth, MipLevels = mipLevels, GuestFormat = guestFormat, Format = format, @@ -12260,14 +12414,19 @@ internal static unsafe class VulkanVideoPresenter _guestImageExtents[target.Address] = ( target.Width, target.Height, - GetTextureByteCount(target.Format, target.Width, target.Height)); + GetTextureByteCount( + target.Format, + target.Width, + target.Height, + depth)); } if (target.Width <= 1920 && target.Height <= 1080) { SharpEmu.HLE.GuestImageWriteTracker.Track( target.Address, - (ulong)target.Width * target.Height * GetTextureBytesPerPixel(target.Format), + (ulong)target.Width * target.Height * depth * + GetTextureBytesPerPixel(target.Format), CurrentGuestWorkSequenceForDiagnostics, "vulkan.render-target"); } @@ -12740,9 +12899,13 @@ internal static unsafe class VulkanVideoPresenter return renderPass; } - private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels) + private static uint ClampMipLevels( + uint width, + uint height, + uint depth, + uint requestedMipLevels) { - var largestDimension = Math.Max(width, height); + var largestDimension = Math.Max(Math.Max(width, height), depth); uint maximumMipLevels = 1; while (largestDimension > 1) { @@ -12753,6 +12916,11 @@ internal static unsafe class VulkanVideoPresenter return Math.Min(Math.Max(requestedMipLevels, 1u), maximumMipLevels); } + private static uint GetMipDimension(uint dimension, uint mipLevel) => + mipLevel >= 32 + ? 1 + : Math.Max(dimension >> (int)mipLevel, 1u); + private void DestroyGuestImage(GuestImageResource resource) { foreach (var depthFramebuffer in resource.DepthFramebuffers.Values) @@ -12929,7 +13097,7 @@ internal static unsafe class VulkanVideoPresenter { SType = StructureType.ImageViewCreateInfo, Image = resource.Image, - ViewType = arrayedView ? ImageViewType.Type2DArray : ImageViewType.Type2D, + ViewType = GetGuestTextureViewType(resource.Type, arrayedView), Format = format, Components = ToVkComponentMapping(dstSelect), SubresourceRange = ColorSubresourceRange(mipLevel, levelCount), @@ -14060,7 +14228,10 @@ internal static unsafe class VulkanVideoPresenter SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, DstQueueFamilyIndex = Vk.QueueFamilyIgnored, Image = texture.Image, - SubresourceRange = ColorSubresourceRange(layerCount: texture.Layers), + SubresourceRange = ColorSubresourceRange( + texture.MipLevel, + 1, + texture.Layers), }; _vk.CmdPipelineBarrier( _commandBuffer, @@ -14084,9 +14255,13 @@ internal static unsafe class VulkanVideoPresenter ImageSubresource = new ImageSubresourceLayers { AspectMask = ImageAspectFlags.ColorBit, + MipLevel = texture.MipLevel, LayerCount = texture.Layers, }, - ImageExtent = new Extent3D(texture.Width, texture.Height, 1), + ImageExtent = new Extent3D( + texture.Width, + texture.Height, + texture.Depth), }; _vk.CmdCopyBufferToImage( _commandBuffer, @@ -14106,7 +14281,10 @@ internal static unsafe class VulkanVideoPresenter SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, DstQueueFamilyIndex = Vk.QueueFamilyIgnored, Image = texture.Image, - SubresourceRange = ColorSubresourceRange(layerCount: texture.Layers), + SubresourceRange = ColorSubresourceRange( + texture.MipLevel, + 1, + texture.Layers), }; _vk.CmdPipelineBarrier( _commandBuffer, diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 3d5d216..05e7bb0 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -314,7 +314,8 @@ public static partial class Gen5SpirvTranslator uint VectorType, ImageComponentKind ComponentKind, bool IsStorage, - bool Arrayed); + bool Arrayed, + SpirvImageDim Dimension); private readonly record struct SpirvVertexInput( uint Variable, @@ -1001,11 +1002,15 @@ public static partial class Gen5SpirvTranslator SpirvCapability.StorageImageExtendedFormats); } - var isArrayed = !isStorage && + var dimension = binding.Control.Dimension == 2 + ? SpirvImageDim.Dim3D + : SpirvImageDim.Dim2D; + var isArrayed = dimension != SpirvImageDim.Dim3D && + !isStorage && Gen5ShaderTranslator.IsArrayedImageBinding(binding); var imageType = _module.TypeImage( componentType, - SpirvImageDim.Dim2D, + dimension, depth: false, arrayed: isArrayed, multisampled: false, @@ -1035,7 +1040,8 @@ public static partial class Gen5SpirvTranslator _module.TypeVector(componentType, 4), componentKind, isStorage, - isArrayed)); + isArrayed, + dimension)); _interfaces.Add(variable); } } @@ -2259,20 +2265,39 @@ public static partial class Gen5SpirvTranslator return false; } + var memoryOpcode = control.UsesFlatAddress + ? "Global" + instruction.Opcode["Flat".Length..] + : instruction.Opcode; + var vectorByteAddress = LoadV(control.VectorAddress); + if (control.UsesFlatAddress) + { + // FLAT instructions carry the complete 64-bit guest address + // in a VGPR pair. The scalar evaluator captures the buffer + // rooted at the inferred SGPR pair, so convert the low address + // dword back to a byte offset inside that binding. Subtraction + // modulo 2^32 also handles an address addition that carried + // into the high dword, while the bounded binding prevents an + // unrelated pointer from escaping into host storage. + vectorByteAddress = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + vectorByteAddress, + LoadS(control.ScalarAddress)); + } var byteAddress = IAdd( - LoadV(control.VectorAddress), + vectorByteAddress, UInt(unchecked((uint)control.OffsetBytes))); byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress); var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); - if (instruction.Opcode is "GlobalAtomicAdd" or "GlobalAtomicUMax") + if (memoryOpcode is "GlobalAtomicAdd" or "GlobalAtomicUMax") { EmitExecConditional(() => { EmitConditional(IsBufferWordInRange(bindingIndex, dwordAddress), () => { var original = _module.AddInstruction( - instruction.Opcode == "GlobalAtomicAdd" + memoryOpcode == "GlobalAtomicAdd" ? SpirvOp.AtomicIAdd : SpirvOp.AtomicUMax, _uintType, @@ -2289,12 +2314,12 @@ public static partial class Gen5SpirvTranslator return true; } - if (instruction.Opcode.StartsWith("GlobalStore", StringComparison.Ordinal)) + if (memoryOpcode.StartsWith("GlobalStore", StringComparison.Ordinal)) { EmitExecConditional(() => { if (TryGetSubdwordStoreInfo( - instruction.Opcode, + memoryOpcode, out var byteCount, out var sourceShift)) { @@ -2324,7 +2349,7 @@ public static partial class Gen5SpirvTranslator } if (TryGetSubdwordLoadInfo( - instruction.Opcode, + memoryOpcode, out var loadByteCount, out var signExtend, out var d16, @@ -3268,6 +3293,7 @@ public static partial class Gen5SpirvTranslator var imageObject = Load(resource.ObjectType, resource.Variable); if (instruction.Opcode == "ImageGetResinfo") { + var sizeComponentCount = ImageCoordinateComponentCount(resource); var queryImage = resource.IsStorage ? imageObject : _module.AddInstruction( @@ -3278,7 +3304,7 @@ public static partial class Gen5SpirvTranslator resource.IsStorage ? SpirvOp.ImageQuerySize : SpirvOp.ImageQuerySizeLod, - _module.TypeVector(_intType, 2), + _module.TypeVector(_intType, sizeComponentCount), resource.IsStorage ? [queryImage] : [queryImage, UInt(0)]); @@ -3291,7 +3317,7 @@ public static partial class Gen5SpirvTranslator } uint value; - if (component < 2) + if (component < sizeComponentCount) { var signedValue = _module.AddInstruction( SpirvOp.CompositeExtract, @@ -3319,7 +3345,12 @@ public static partial class Gen5SpirvTranslator return false; } - var coordinates = BuildIntegerCoordinates(image, 0); + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); + var coordinates = BuildIntegerCoordinates( + image, + 0, + coordinateComponentCount); var components = new uint[4]; uint sourceIndex = 0; for (var component = 0; component < components.Length; component++) @@ -3355,13 +3386,14 @@ public static partial class Gen5SpirvTranslator components); var imageSize = _module.AddInstruction( SpirvOp.ImageQuerySize, - _module.TypeVector(_intType, 2), + _module.TypeVector(_intType, coordinateComponentCount), imageObject); EmitBoundsCheckedImageWrite( coordinates, imageSize, imageObject, - texel); + texel, + coordinateComponentCount); return true; } @@ -3382,14 +3414,17 @@ public static partial class Gen5SpirvTranslator } var signed = resource.ComponentKind == ImageComponentKind.Sint; + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); var atomicImageSize = _module.AddInstruction( SpirvOp.ImageQuerySize, - _module.TypeVector(_intType, 2), + _module.TypeVector(_intType, coordinateComponentCount), imageObject); var coordinates = BuildClampedIntegerCoordinates( image, 0, - atomicImageSize); + atomicImageSize, + coordinateComponentCount); EmitExecConditional(() => { var pointer = _module.AddInstruction( @@ -3433,14 +3468,17 @@ public static partial class Gen5SpirvTranslator { if (resource.IsStorage) { + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); var imageSize = _module.AddInstruction( SpirvOp.ImageQuerySize, - _module.TypeVector(_intType, 2), + _module.TypeVector(_intType, coordinateComponentCount), imageObject); var coordinates = BuildClampedIntegerCoordinates( image, 0, - imageSize); + imageSize, + coordinateComponentCount); sampled = _module.AddInstruction( SpirvOp.ImageRead, resource.VectorType, @@ -3454,15 +3492,18 @@ public static partial class Gen5SpirvTranslator SpirvOp.Image, resource.ImageType, imageObject); + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); var imageSize = _module.AddInstruction( SpirvOp.ImageQuerySizeLod, - _module.TypeVector(_intType, 2), + _module.TypeVector(_intType, coordinateComponentCount), fetchedImage, UInt(mipLevel)); var coordinates = BuildClampedIntegerCoordinates( image, 0, - imageSize); + imageSize, + coordinateComponentCount); sampled = _module.AddInstruction( SpirvOp.ImageFetch, resource.VectorType, @@ -3494,12 +3535,19 @@ public static partial class Gen5SpirvTranslator // lowering treated SAMPLE_D as body-first and consequently // sampled gradients as coordinates in every captured // derivative operation. + var spatialComponentCount = + ImageSpatialComponentCount(resource); + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); var addressCursor = 0; var offset = 0u; if (hasOffset) { addressCursor = AlignFullImageAddress(image, addressCursor); - offset = BuildImageOffset(image, addressCursor); + offset = BuildImageOffset( + image, + addressCursor, + spatialComponentCount); addressCursor += ImageFullAddressSlots(image); } @@ -3523,26 +3571,33 @@ public static partial class Gen5SpirvTranslator } var gradientX = hasGradients - ? BuildFloatCoordinates(image, addressCursor) + ? BuildFloatCoordinates( + image, + addressCursor, + spatialComponentCount) : 0u; var gradientY = hasGradients - ? BuildFloatCoordinates(image, addressCursor + 2) + ? BuildFloatCoordinates( + image, + addressCursor + (int)spatialComponentCount, + spatialComponentCount) : 0u; if (hasGradients) { - addressCursor += 4; + addressCursor += checked((int)(spatialComponentCount * 2)); } - var coordinates = resource.Arrayed - ? BuildFloatArrayCoordinates(image, addressCursor) - : BuildFloatCoordinates(image, addressCursor); + var coordinates = BuildFloatCoordinates( + image, + addressCursor, + coordinateComponentCount); var explicitLod = hasGradients || hasZeroLod || hasLod; var lod = hasZeroLod ? Float(0) : hasLod ? LoadImageFloatAddress( image, - addressCursor + (resource.Arrayed ? 3 : 2)) + addressCursor + (int)coordinateComponentCount) : lodOrBias; if (hasOffset) { @@ -3608,11 +3663,18 @@ public static partial class Gen5SpirvTranslator instruction.Opcode.EndsWith("O", StringComparison.Ordinal); var hasCompare = instruction.Opcode.Contains("Gather4C", StringComparison.Ordinal); + var spatialComponentCount = + ImageSpatialComponentCount(resource); + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); var addressCursor = 0; var offset = 0u; if (hasOffset) { - offset = BuildImageOffset(image, addressCursor); + offset = BuildImageOffset( + image, + addressCursor, + spatialComponentCount); addressCursor += ImageFullAddressSlots(image); } @@ -3627,9 +3689,10 @@ public static partial class Gen5SpirvTranslator addressCursor += ImageFullAddressSlots(image); } - var coordinates = resource.Arrayed - ? BuildFloatArrayCoordinates(image, addressCursor) - : BuildFloatCoordinates(image, addressCursor); + var coordinates = BuildFloatCoordinates( + image, + addressCursor, + coordinateComponentCount); var operands = new List { imageObject, @@ -3822,28 +3885,31 @@ public static partial class Gen5SpirvTranslator }); } - private uint BuildFloatCoordinates(Gen5ImageControl image, int start) - { - var x = LoadImageFloatAddress(image, start); - var y = LoadImageFloatAddress(image, start + 1); - return _module.AddInstruction( - SpirvOp.CompositeConstruct, - _vec2Type, - x, - y); - } + private static uint ImageSpatialComponentCount( + SpirvImageResource resource) => + resource.Dimension == SpirvImageDim.Dim3D ? 3u : 2u; - private uint BuildFloatArrayCoordinates(Gen5ImageControl image, int start) + private static uint ImageCoordinateComponentCount( + SpirvImageResource resource) => + resource.Arrayed ? 3u : ImageSpatialComponentCount(resource); + + private uint BuildFloatCoordinates( + Gen5ImageControl image, + int start, + uint componentCount) { - var x = LoadImageFloatAddress(image, start); - var y = LoadImageFloatAddress(image, start + 1); - var slice = LoadImageFloatAddress(image, start + 2); + var components = new uint[checked((int)componentCount)]; + for (var component = 0; component < components.Length; component++) + { + components[component] = LoadImageFloatAddress( + image, + start + component); + } + return _module.AddInstruction( SpirvOp.CompositeConstruct, - _vec3Type, - x, - y, - slice); + _module.TypeVector(_floatType, componentCount), + components); } private static int ImageAddressRegister( @@ -3948,47 +4014,49 @@ public static partial class Gen5SpirvTranslator ShiftLeftLogical(BitwiseAnd(high, UInt(0xFFFF)), UInt(16))); } - private uint BuildIntegerCoordinates(Gen5ImageControl image, int start) + private uint BuildIntegerCoordinates( + Gen5ImageControl image, + int start, + uint componentCount) { - var ivec2 = _module.TypeVector(_intType, 2); - var x = Bitcast(_intType, LoadImageIntegerAddress(image, start)); - var y = Bitcast(_intType, LoadImageIntegerAddress(image, start + 1)); + var components = new uint[checked((int)componentCount)]; + for (var component = 0; component < components.Length; component++) + { + components[component] = Bitcast( + _intType, + LoadImageIntegerAddress(image, start + component)); + } + return _module.AddInstruction( SpirvOp.CompositeConstruct, - ivec2, - x, - y); + _module.TypeVector(_intType, componentCount), + components); } private uint BuildClampedIntegerCoordinates( Gen5ImageControl image, int start, - uint imageSize) + uint imageSize, + uint componentCount) { - var ivec2 = _module.TypeVector(_intType, 2); - var x = ClampSignedCoordinate( - Bitcast( - _intType, - LoadImageIntegerAddress(image, start)), - _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - imageSize, - 0)); - var y = ClampSignedCoordinate( - Bitcast( - _intType, - LoadImageIntegerAddress(image, start + 1)), - _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - imageSize, - 1)); + var components = new uint[checked((int)componentCount)]; + for (var component = 0; component < components.Length; component++) + { + components[component] = ClampSignedCoordinate( + Bitcast( + _intType, + LoadImageIntegerAddress(image, start + component)), + _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + imageSize, + (uint)component)); + } + return _module.AddInstruction( SpirvOp.CompositeConstruct, - ivec2, - x, - y); + _module.TypeVector(_intType, componentCount), + components); } private uint ClampSignedCoordinate(uint value, uint extent) @@ -4027,69 +4095,47 @@ public static partial class Gen5SpirvTranslator uint coordinates, uint imageSize, uint imageObject, - uint texel) + uint texel, + uint coordinateComponentCount) { - var x = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - coordinates, - 0); - var y = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - coordinates, - 1); - var width = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - imageSize, - 0); - var height = _module.AddInstruction( - SpirvOp.CompositeExtract, - _intType, - imageSize, - 1); var zero = _module.Constant(_intType, 0); - var xNonNegative = _module.AddInstruction( - SpirvOp.SGreaterThanEqual, - _boolType, - x, - zero); - var yNonNegative = _module.AddInstruction( - SpirvOp.SGreaterThanEqual, - _boolType, - y, - zero); - var xInRange = _module.AddInstruction( - SpirvOp.SLessThan, - _boolType, - x, - width); - var yInRange = _module.AddInstruction( - SpirvOp.SLessThan, - _boolType, - y, - height); - var lowerInRange = _module.AddInstruction( - SpirvOp.LogicalAnd, - _boolType, - xNonNegative, - yNonNegative); - var upperInRange = _module.AddInstruction( - SpirvOp.LogicalAnd, - _boolType, - xInRange, - yInRange); - var inRange = _module.AddInstruction( - SpirvOp.LogicalAnd, - _boolType, - lowerInRange, - upperInRange); - inRange = _module.AddInstruction( - SpirvOp.LogicalAnd, - _boolType, - Load(_boolType, _exec), - inRange); + var inRange = Load(_boolType, _exec); + for (uint component = 0; + component < coordinateComponentCount; + component++) + { + var coordinate = _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + coordinates, + component); + var extent = _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + imageSize, + component); + var nonNegative = _module.AddInstruction( + SpirvOp.SGreaterThanEqual, + _boolType, + coordinate, + zero); + var belowExtent = _module.AddInstruction( + SpirvOp.SLessThan, + _boolType, + coordinate, + extent); + var componentInRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + nonNegative, + belowExtent); + inRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + inRange, + componentInRange); + } + var writeLabel = _module.AllocateId(); var mergeLabel = _module.AllocateId(); _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); @@ -4108,30 +4154,30 @@ public static partial class Gen5SpirvTranslator _module.AddLabel(mergeLabel); } - private uint BuildImageOffset(Gen5ImageControl image, int component) + private uint BuildImageOffset( + Gen5ImageControl image, + int component, + uint componentCount) { - var ivec2 = _module.TypeVector(_intType, 2); var packed = Bitcast( _intType, LoadV(image.GetAddressRegister( ImageAddressRegister(image, component)))); - var x = _module.AddInstruction( - SpirvOp.BitFieldSExtract, - _intType, - packed, - UInt(0), - UInt(6)); - var y = _module.AddInstruction( - SpirvOp.BitFieldSExtract, - _intType, - packed, - UInt(8), - UInt(6)); + var components = new uint[checked((int)componentCount)]; + for (var index = 0; index < components.Length; index++) + { + components[index] = _module.AddInstruction( + SpirvOp.BitFieldSExtract, + _intType, + packed, + UInt((uint)(index * 8)), + UInt(6)); + } + return _module.AddInstruction( SpirvOp.CompositeConstruct, - ivec2, - x, - y); + _module.TypeVector(_intType, componentCount), + components); } private uint ApplyDynamicSampleOffset( @@ -4141,7 +4187,16 @@ public static partial class Gen5SpirvTranslator uint texelOffset, uint lod) { - var ivec2 = _module.TypeVector(_intType, 2); + var spatialComponentCount = ImageSpatialComponentCount(resource); + var coordinateComponentCount = + ImageCoordinateComponentCount(resource); + var spatialIntegerType = + _module.TypeVector(_intType, spatialComponentCount); + var spatialFloatType = + _module.TypeVector(_floatType, spatialComponentCount); + var queryComponentCount = resource.Arrayed + ? coordinateComponentCount + : spatialComponentCount; var image = _module.AddInstruction( SpirvOp.Image, resource.ImageType, @@ -4163,14 +4218,14 @@ public static partial class Gen5SpirvTranslator signedLod); var size = _module.AddInstruction( SpirvOp.ImageQuerySizeLod, - resource.Arrayed ? _module.TypeVector(_intType, 3) : ivec2, + _module.TypeVector(_intType, queryComponentCount), image, clampedLod); if (resource.Arrayed) { size = _module.AddInstruction( SpirvOp.VectorShuffle, - ivec2, + spatialIntegerType, size, size, 0u, @@ -4179,27 +4234,27 @@ public static partial class Gen5SpirvTranslator var sizeFloat = _module.AddInstruction( SpirvOp.ConvertSToF, - _vec2Type, + spatialFloatType, size); var offsetFloat = _module.AddInstruction( SpirvOp.ConvertSToF, - _vec2Type, + spatialFloatType, texelOffset); var normalizedOffset = _module.AddInstruction( SpirvOp.FDiv, - _vec2Type, + spatialFloatType, offsetFloat, sizeFloat); if (!resource.Arrayed) { return _module.AddInstruction( SpirvOp.FAdd, - _vec2Type, + spatialFloatType, coordinates, normalizedOffset); } - var offsetVec3 = _module.AddInstruction( + var arrayOffset = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec3Type, _module.AddInstruction( @@ -4217,7 +4272,7 @@ public static partial class Gen5SpirvTranslator SpirvOp.FAdd, _vec3Type, coordinates, - offsetVec3); + arrayOffset); } private bool TryEmitExport( diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index d00a6e8..ad46c71 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -192,7 +192,8 @@ public sealed record Gen5GlobalMemoryControl( uint ScalarAddress, int OffsetBytes, bool Glc, - bool Slc) : Gen5InstructionControl; + bool Slc, + bool UsesFlatAddress = false) : Gen5InstructionControl; public sealed record Gen5BufferMemoryControl( uint DwordCount, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index 369ecf1..d7d6775 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -358,7 +358,10 @@ public static class Gen5ShaderScalarEvaluator if (globalMemory.ScalarAddress >= ScalarRegisterCount - 1) { error = - $"global-address-register-range pc=0x{instruction.Pc:X} " + + $"{(globalMemory.UsesFlatAddress + ? "flat-address-base-unresolved" + : "global-address-register-range")} " + + $"pc=0x{instruction.Pc:X} " + $"s{globalMemory.ScalarAddress}"; return false; } @@ -373,11 +376,18 @@ public static class Gen5ShaderScalarEvaluator } var key = (globalMemory.ScalarAddress, baseAddress); - var writable = instruction.Opcode.StartsWith( + var writable = + instruction.Opcode.StartsWith( "GlobalStore", StringComparison.Ordinal) || instruction.Opcode.StartsWith( "GlobalAtomic", + StringComparison.Ordinal) || + instruction.Opcode.StartsWith( + "FlatStore", + StringComparison.Ordinal) || + instruction.Opcode.StartsWith( + "FlatAtomic", StringComparison.Ordinal); if (globalMemoryByAddress.TryGetValue(key, out var existingBinding)) { diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 6317427..f81a0db 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -471,7 +471,15 @@ public static class Gen5ShaderTranslator } } - instructions.Add(CreateInstruction(pc, encoding, name, words)); + var instruction = CreateInstruction(pc, encoding, name, words); + if (instruction.Control is Gen5GlobalMemoryControl + { + UsesFlatAddress: true, + }) + { + instruction = ResolveFlatAddressBase(instructions, instruction); + } + instructions.Add(instruction); instructionCount++; pc += sizeDwords * sizeof(uint); @@ -1395,35 +1403,42 @@ public static class Gen5ShaderTranslator var opcode = (word >> 18) & 0x7F; sizeDwords = 2; error = string.Empty; - name = segment == 0x2 - ? opcode switch - { - 0x08 => "GlobalLoadUbyte", - 0x09 => "GlobalLoadSbyte", - 0x0A => "GlobalLoadUshort", - 0x0B => "GlobalLoadSshort", - 0x0C => "GlobalLoadDword", - 0x0D => "GlobalLoadDwordx2", - 0x0E => "GlobalLoadDwordx4", - 0x0F => "GlobalLoadDwordx3", - 0x18 => "GlobalStoreByte", - 0x19 => "GlobalStoreByteD16Hi", - 0x1A => "GlobalStoreShort", - 0x1B => "GlobalStoreShortD16Hi", - 0x1C => "GlobalStoreDword", - 0x1D => "GlobalStoreDwordx2", - 0x1E => "GlobalStoreDwordx4", - 0x1F => "GlobalStoreDwordx3", - 0x20 => "GlobalLoadUbyteD16", - 0x21 => "GlobalLoadUbyteD16Hi", - 0x22 => "GlobalLoadSbyteD16", - 0x23 => "GlobalLoadSbyteD16Hi", - 0x24 => "GlobalLoadShortD16", - 0x25 => "GlobalLoadShortD16Hi", - 0x32 => "GlobalAtomicAdd", - 0x38 => "GlobalAtomicUMax", - _ => string.Empty, - } + var prefix = segment switch + { + 0x0 => "Flat", + 0x2 => "Global", + _ => string.Empty, + }; + var suffix = opcode switch + { + 0x08 => "LoadUbyte", + 0x09 => "LoadSbyte", + 0x0A => "LoadUshort", + 0x0B => "LoadSshort", + 0x0C => "LoadDword", + 0x0D => "LoadDwordx2", + 0x0E => "LoadDwordx4", + 0x0F => "LoadDwordx3", + 0x18 => "StoreByte", + 0x19 => "StoreByteD16Hi", + 0x1A => "StoreShort", + 0x1B => "StoreShortD16Hi", + 0x1C => "StoreDword", + 0x1D => "StoreDwordx2", + 0x1E => "StoreDwordx4", + 0x1F => "StoreDwordx3", + 0x20 => "LoadUbyteD16", + 0x21 => "LoadUbyteD16Hi", + 0x22 => "LoadSbyteD16", + 0x23 => "LoadSbyteD16Hi", + 0x24 => "LoadShortD16", + 0x25 => "LoadShortD16Hi", + 0x32 => "AtomicAdd", + 0x38 => "AtomicUMax", + _ => string.Empty, + }; + name = prefix.Length != 0 && suffix.Length != 0 + ? prefix + suffix : string.Empty; return FinishDecode( @@ -1646,6 +1661,82 @@ public static class Gen5ShaderTranslator _ => false, }; + private static Gen5ShaderInstruction ResolveFlatAddressBase( + IReadOnlyList precedingInstructions, + Gen5ShaderInstruction instruction) + { + if (instruction.Control is not Gen5GlobalMemoryControl + { + UsesFlatAddress: true, + } control || + !TryFindVectorDefinition( + precedingInstructions, + control.VectorAddress, + out var lowDefinition) || + !TryFindVectorDefinition( + precedingInstructions, + control.VectorAddress + 1, + out var highDefinition)) + { + return instruction; + } + + foreach (var lowSource in lowDefinition.Sources) + { + if (lowSource.Kind != Gen5OperandKind.ScalarRegister) + { + continue; + } + + foreach (var highSource in highDefinition.Sources) + { + if (highSource.Kind != Gen5OperandKind.ScalarRegister || + highSource.Value != lowSource.Value + 1) + { + continue; + } + + return instruction with + { + Sources = + [ + .. instruction.Sources, + Gen5Operand.Scalar(lowSource.Value), + ], + Control = control with + { + ScalarAddress = lowSource.Value, + }, + }; + } + } + + return instruction; + } + + private static bool TryFindVectorDefinition( + IReadOnlyList instructions, + uint register, + out Gen5ShaderInstruction definition) + { + for (var index = instructions.Count - 1; index >= 0; index--) + { + var candidate = instructions[index]; + foreach (var destination in candidate.Destinations) + { + if (destination.Kind == Gen5OperandKind.VectorRegister && + destination.Value == register) + { + definition = candidate; + return true; + } + } + } + + definition = default!; + return false; + } + private static Gen5ShaderInstruction CreateInstruction( uint pc, Gen5ShaderEncoding encoding, @@ -2057,7 +2148,13 @@ public static class Gen5ShaderTranslator var vectorAddress = extra & 0xFF; var vectorData = (extra >> 8) & 0xFF; var scalarAddress = (extra >> 16) & 0x7F; - var dwordCount = opcode switch + var usesFlatAddress = opcode.StartsWith( + "Flat", + StringComparison.Ordinal); + var memoryOpcode = usesFlatAddress + ? "Global" + opcode["Flat".Length..] + : opcode; + var dwordCount = memoryOpcode switch { "GlobalLoadUbyte" or "GlobalLoadSbyte" or @@ -2085,12 +2182,20 @@ public static class Gen5ShaderTranslator "GlobalStoreDwordx4" => 4u, _ => 0u, }; - sources = - [ - Gen5Operand.Vector(vectorAddress), - Gen5Operand.Scalar(scalarAddress), - ]; - destinations = opcode.StartsWith("GlobalLoad", StringComparison.Ordinal) + sources = usesFlatAddress + ? + [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorAddress + 1), + ] + : + [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Scalar(scalarAddress), + ]; + destinations = memoryOpcode.StartsWith( + "GlobalLoad", + StringComparison.Ordinal) ? Enumerable .Range((int)vectorData, checked((int)dwordCount)) .Select(index => Gen5Operand.Vector((uint)index)) @@ -2100,10 +2205,11 @@ public static class Gen5ShaderTranslator dwordCount, vectorAddress, vectorData, - scalarAddress, + usesFlatAddress ? uint.MaxValue : scalarAddress, SignExtend(word & 0x1FFF, 13), ((word >> 16) & 1) != 0, - ((word >> 17) & 1) != 0); + ((word >> 17) & 1) != 0, + usesFlatAddress); break; } case Gen5ShaderEncoding.Mubuf: diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcTextureTransportTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcTextureTransportTests.cs new file mode 100644 index 0000000..c96069b --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcTextureTransportTests.cs @@ -0,0 +1,106 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Agc; +using SharpEmu.Libs.Gpu; +using Xunit; + +namespace SharpEmu.Libs.Tests.Agc; + +public sealed class AgcTextureTransportTests +{ + [Theory] + [InlineData(10u, 4u, 4u)] + [InlineData(10u, 0u, 1u)] + [InlineData(9u, 4u, 1u)] + [InlineData(13u, 4u, 1u)] + public void GetTextureVolumeDepth_OnlyUsesDescriptorDepthFor3D( + uint type, + uint descriptorDepth, + uint expectedDepth) + { + Assert.Equal( + expectedDepth, + AgcExports.GetTextureVolumeDepth(type, descriptorDepth)); + } + + [Fact] + public void GetTextureByteCount_MultipliesUncompressedVolumeDepth() + { + Assert.Equal( + 4UL * 8 * 6 * 5, + AgcExports.GetTextureByteCount( + format: 10, + width: 8, + height: 6, + depth: 5)); + } + + [Fact] + public void GetTextureByteCount_MultipliesBlockCompressedVolumeDepth() + { + // Format 169 uses one eight-byte BC block for each 4x4 texel block. + Assert.Equal( + 2UL * 2 * 8 * 3, + AgcExports.GetTextureByteCount( + format: 169, + width: 7, + height: 5, + depth: 3)); + } + + [Fact] + public void GetTextureByteCount_LeavesTwoDimensionalSizingUnchanged() + { + Assert.Equal( + AgcExports.GetTextureByteCount(10, 8, 6), + AgcExports.GetTextureByteCount(10, 8, 6, depth: 1)); + Assert.Equal( + AgcExports.GetTextureByteCount(10, 8, 6), + AgcExports.GetTextureByteCount(10, 8, 6, depth: 0)); + } + + [Fact] + public void GuestDrawTexture_CarriesRawTypeAndNormalizedDepth() + { + var texture = new GuestDrawTexture( + Address: 0x1234, + Width: 8, + Height: 6, + Format: 10, + NumberType: 0, + RgbaPixels: [], + IsFallback: false, + IsStorage: false, + Type: 10, + Depth: 5); + + Assert.Equal(10u, texture.Type); + Assert.Equal(5u, texture.Depth); + } + + [Fact] + public void TextureContentIdentity_DistinguishesTypeAndDepth() + { + var twoDimensional = CreateIdentity(type: 9, depth: 1); + var threeDimensional = CreateIdentity(type: 10, depth: 1); + var deeperThreeDimensional = CreateIdentity(type: 10, depth: 5); + + Assert.NotEqual(twoDimensional, threeDimensional); + Assert.NotEqual(threeDimensional, deeperThreeDimensional); + } + + private static TextureContentIdentity CreateIdentity(uint type, uint depth) => + new( + Address: 0x1234, + Width: 8, + Height: 6, + Format: 10, + NumberType: 0, + DstSelect: 0xFAC, + TileMode: 0, + Pitch: 8, + Sampler: default, + Type: type, + Depth: depth); +} diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageByteCountTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageByteCountTests.cs index f27d93c..91a75b5 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageByteCountTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageByteCountTests.cs @@ -36,4 +36,23 @@ public sealed class VulkanGuestImageByteCountTests expected, VulkanVideoPresenter.GetGuestImageByteCount(format, width, height)); } + + [Theory] + [InlineData(10u, 8u, 4u, 3u, 384UL)] + [InlineData(169u, 5u, 5u, 7u, 224UL)] + public void MultipliesSurfaceSizeByVolumeDepth( + uint format, + uint width, + uint height, + uint depth, + ulong expected) + { + Assert.Equal( + expected, + VulkanVideoPresenter.GetGuestImageByteCount( + format, + width, + height, + depth)); + } } diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageTypeTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageTypeTests.cs new file mode 100644 index 0000000..5e575a2 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanGuestImageTypeTests.cs @@ -0,0 +1,49 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using Silk.NET.Vulkan; +using SharpEmu.Libs.VideoOut; +using Xunit; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed class VulkanGuestImageTypeTests +{ + [Fact] + public void ThreeDimensionalDescriptorsMapToVolumeImageAndViewTypes() + { + Assert.Equal( + ImageType.Type3D, + VulkanVideoPresenter.GetGuestTextureImageType( + VulkanVideoPresenter.Gen5TextureType3D)); + Assert.Equal( + ImageViewType.Type3D, + VulkanVideoPresenter.GetGuestTextureViewType( + VulkanVideoPresenter.Gen5TextureType3D, + arrayedView: true)); + Assert.Equal( + 7u, + VulkanVideoPresenter.GetGuestTextureDepth( + VulkanVideoPresenter.Gen5TextureType3D, + 7)); + } + + [Fact] + public void TwoDimensionalArraysKeepLayersSeparateFromImageDepth() + { + Assert.Equal( + ImageType.Type2D, + VulkanVideoPresenter.GetGuestTextureImageType( + VulkanVideoPresenter.Gen5TextureType2D)); + Assert.Equal( + ImageViewType.Type2DArray, + VulkanVideoPresenter.GetGuestTextureViewType( + VulkanVideoPresenter.Gen5TextureType2D, + arrayedView: true)); + Assert.Equal( + 1u, + VulkanVideoPresenter.GetGuestTextureDepth( + VulkanVideoPresenter.Gen5TextureType2D, + 7)); + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs new file mode 100644 index 0000000..f9fafd5 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5FlatMemoryTests.cs @@ -0,0 +1,179 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class Gen5FlatMemoryTests +{ + private const ulong ShaderAddress = 0x1_0000_0000; + private const uint SEndpgm = 0xBF810000; + + [Fact] + public void FlatLoadUbyteInfersScalarBaseAndCompiles() + { + var memory = new TestCpuMemory(ShaderAddress, 0x4000); + uint[] words = + [ + // v_add_co_u32 v1, vcc_lo, s12, v6 + 0xD70F6A01, + 0x00020C0C, + // v_add_co_ci_u32_sdwa v2, vcc_lo, 0, s13, vcc_lo + 0x50041AF9, + 0x86860680, + // flat_load_ubyte v0, v[1:2] + 0xDC200000, + 0x007D0001, + SEndpgm, + ]; + var shader = new byte[words.Length * sizeof(uint)]; + for (var index = 0; index < words.Length; index++) + { + BinaryPrimitives.WriteUInt32LittleEndian( + shader.AsSpan(index * sizeof(uint)), + words[index]); + } + Assert.True(memory.TryWrite(ShaderAddress, shader)); + + var ctx = new CpuContext(memory, Generation.Gen5); + Assert.True( + Gen5ShaderTranslator.TryDecodeProgram( + ctx, + ShaderAddress, + out var program, + out var decodeError), + decodeError); + + var instruction = Assert.Single( + program.Instructions, + item => item.Opcode == "FlatLoadUbyte"); + var control = Assert.IsType( + instruction.Control); + Assert.True(control.UsesFlatAddress); + Assert.Equal(1u, control.VectorAddress); + Assert.Equal(0u, control.VectorData); + Assert.Equal(12u, control.ScalarAddress); + Assert.Equal( + [ + Gen5Operand.Vector(1), + Gen5Operand.Vector(2), + Gen5Operand.Scalar(12), + ], + instruction.Sources); + + uint[] userData = + [ + unchecked((uint)ShaderAddress), + unchecked((uint)(ShaderAddress >> 32)), + ]; + var state = new Gen5ShaderState( + program, + userData, + null, + UserDataScalarRegisterBase: 12); + Assert.True( + Gen5ShaderScalarEvaluator.TryEvaluate( + ctx, + state, + out var evaluation, + out var evaluationError), + evaluationError); + + var binding = Assert.Single(evaluation.GlobalMemoryBindings); + Assert.Equal(12u, binding.ScalarAddress); + Assert.Contains(instruction.Pc, binding.InstructionPcs); + Assert.True( + Gen5SpirvTranslator.TryCompileComputeShader( + state, + evaluation, + 1, + 1, + 1, + out var compiled, + out var compileError), + compileError); + Assert.Contains( + (ushort)SpirvOp.ISub, + ReadSpirvOpcodes(compiled.Spirv)); + } + + private static IReadOnlyList ReadSpirvOpcodes(byte[] spirv) + { + Assert.Equal(0, spirv.Length % sizeof(uint)); + Assert.True(spirv.Length >= 5 * sizeof(uint)); + Assert.Equal( + 0x07230203u, + BinaryPrimitives.ReadUInt32LittleEndian(spirv)); + + var opcodes = new List(); + for (var offset = 5 * sizeof(uint); offset < spirv.Length;) + { + var instruction = + BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset)); + var wordCount = checked((int)(instruction >> 16)); + Assert.InRange( + wordCount, + 1, + (spirv.Length - offset) / sizeof(uint)); + opcodes.Add((ushort)instruction); + offset += wordCount * sizeof(uint); + } + + return opcodes; + } + + private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory + { + private readonly byte[] _storage = new byte[size]; + + public bool TryRead(ulong virtualAddress, Span destination) + { + if (!TryResolve(virtualAddress, destination.Length, out var offset)) + { + return false; + } + + _storage.AsSpan(offset, destination.Length).CopyTo(destination); + return true; + } + + public bool TryWrite( + ulong virtualAddress, + ReadOnlySpan source) + { + if (!TryResolve(virtualAddress, source.Length, out var offset)) + { + return false; + } + + source.CopyTo(_storage.AsSpan(offset, source.Length)); + return true; + } + + private bool TryResolve( + ulong virtualAddress, + int length, + out int offset) + { + offset = 0; + if (virtualAddress < baseAddress) + { + return false; + } + + var relative = virtualAddress - baseAddress; + if (relative + (ulong)length > (ulong)_storage.Length) + { + return false; + } + + offset = (int)relative; + return true; + } + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs new file mode 100644 index 0000000..f926e6e --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs @@ -0,0 +1,197 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Vulkan; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class Gen5ImageTests +{ + private const ulong ShaderAddress = 0x1_0000_C000; + private const uint SEndpgm = 0xBF810000; + + [Theory] + [InlineData(1u, SpirvImageDim.Dim2D, 2u)] + [InlineData(2u, SpirvImageDim.Dim3D, 3u)] + public void ImageStoreDimensionControlsImageAndCoordinateTypes( + uint dimension, + SpirvImageDim expectedImageDimension, + uint expectedCoordinateComponents) + { + var instructions = ReadSpirvInstructions( + CompileImageOperation("ImageStore", dimension)); + var imageType = Assert.Single( + instructions, + item => item.Opcode == SpirvOp.TypeImage); + + Assert.Equal((uint)expectedImageDimension, imageType.Operands[2]); + Assert.Equal(2u, imageType.Operands[6]); + AssertCoordinateVectorWidth( + instructions, + SpirvOp.ImageWrite, + coordinateOperand: 1, + expectedComponents: expectedCoordinateComponents); + + var sizeQuery = Assert.Single( + instructions, + item => item.Opcode == SpirvOp.ImageQuerySize); + AssertVectorTypeWidth( + instructions, + sizeQuery.Operands[0], + expectedCoordinateComponents); + } + + [Fact] + public void ImageSampleDim3DUsesThreeComponentSampleCoordinates() + { + var instructions = ReadSpirvInstructions( + CompileImageOperation("ImageSampleLz", dimension: 2)); + var imageType = Assert.Single( + instructions, + item => item.Opcode == SpirvOp.TypeImage); + + Assert.Equal((uint)SpirvImageDim.Dim3D, imageType.Operands[2]); + Assert.Equal(1u, imageType.Operands[6]); + AssertCoordinateVectorWidth( + instructions, + SpirvOp.ImageSampleExplicitLod, + coordinateOperand: 3, + expectedComponents: 3); + } + + private static byte[] CompileImageOperation(string opcode, uint dimension) + { + var addressRegisters = dimension == 2 + ? new uint[] { 0, 1, 2 } + : [0, 1]; + var control = new Gen5ImageControl( + Dmask: 0xF, + VectorAddress: 0, + AddressRegisters: addressRegisters, + VectorData: 4, + ScalarResource: 8, + ScalarSampler: 16, + Dimension: dimension, + IsArray: false, + Glc: false, + Slc: false, + A16: false, + D16: false); + var imageInstruction = new Gen5ShaderInstruction( + 0, + Gen5ShaderEncoding.Mimg, + opcode, + [], + [], + [], + control); + var end = new Gen5ShaderInstruction( + 8, + Gen5ShaderEncoding.Sopp, + "SEndpgm", + [SEndpgm], + [], + [], + null); + var state = new Gen5ShaderState( + new Gen5ShaderProgram(ShaderAddress, [imageInstruction, end]), + [], + null); + var scalarRegisters = new uint[256]; + var descriptor = new uint[8]; + descriptor[1] = 71u << 20; // FORMAT_16_16_16_16_FLOAT + descriptor[3] = (dimension == 2 ? 10u : 9u) << 28; + var evaluation = new Gen5ShaderEvaluation( + scalarRegisters, + scalarRegisters, + [ + new Gen5ImageBinding( + imageInstruction.Pc, + imageInstruction.Opcode, + control, + descriptor, + new uint[4], + null), + ], + []); + + Assert.True( + Gen5SpirvTranslator.TryCompileComputeShader( + state, + evaluation, + 1, + 1, + 1, + out var shader, + out var error), + error); + return shader.Spirv; + } + + private static IReadOnlyList ReadSpirvInstructions( + byte[] spirv) + { + var instructions = new List(); + for (var offset = 5 * sizeof(uint); offset < spirv.Length;) + { + var instruction = BinaryPrimitives.ReadUInt32LittleEndian( + spirv.AsSpan(offset)); + var wordCount = checked((int)(instruction >> 16)); + Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint)); + var operands = new uint[wordCount - 1]; + for (var operand = 0; operand < operands.Length; operand++) + { + operands[operand] = BinaryPrimitives.ReadUInt32LittleEndian( + spirv.AsSpan(offset + (operand + 1) * sizeof(uint))); + } + + instructions.Add( + new ParsedSpirvInstruction((SpirvOp)(ushort)instruction, operands)); + offset += wordCount * sizeof(uint); + } + + return instructions; + } + + private static void AssertCoordinateVectorWidth( + IReadOnlyList instructions, + SpirvOp operation, + int coordinateOperand, + uint expectedComponents) + { + var imageOperation = Assert.Single( + instructions, + item => item.Opcode == operation); + var coordinateId = imageOperation.Operands[coordinateOperand]; + var coordinate = Assert.Single( + instructions, + item => + item.Opcode == SpirvOp.CompositeConstruct && + item.Operands.Length >= 2 && + item.Operands[1] == coordinateId); + AssertVectorTypeWidth( + instructions, + coordinate.Operands[0], + expectedComponents); + } + + private static void AssertVectorTypeWidth( + IReadOnlyList instructions, + uint vectorTypeId, + uint expectedComponents) + { + var vectorType = Assert.Single( + instructions, + item => + item.Opcode == SpirvOp.TypeVector && + item.Operands[0] == vectorTypeId); + Assert.Equal(expectedComponents, vectorType.Operands[2]); + } + + private readonly record struct ParsedSpirvInstruction( + SpirvOp Opcode, + uint[] Operands); +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/SharpEmu.ShaderCompiler.Tests.csproj b/tests/SharpEmu.ShaderCompiler.Tests/SharpEmu.ShaderCompiler.Tests.csproj new file mode 100644 index 0000000..edaadee --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/SharpEmu.ShaderCompiler.Tests.csproj @@ -0,0 +1,22 @@ + + + + + false + false + + + + + + + + + + + + +