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
+
+
+
+
+
+
+
+
+
+
+
+
+