356 lines
18 KiB
C#
356 lines
18 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE.GpuMemory;
|
|
using SharpEmu.Libs.Gpu.Buffers;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using Silk.NET.Vulkan;
|
|
|
|
namespace SharpEmu.Libs.Gpu.Images;
|
|
|
|
// A texture request built from the descriptor words and the shape the shader was compiled with.
|
|
public readonly record struct TextureRequestResolution(ImageRequest Request, bool ExactFormat, Format ViewFormat, uint Swizzle);
|
|
|
|
public static partial class ImageRequestBuilders
|
|
{
|
|
// A null descriptor binds a one-texel image of the numeric class the shader expects.
|
|
public static ImageRequest NullTexture(in ShaderImageShape shape)
|
|
{
|
|
var numericClass = shape.NumericClass;
|
|
var storage = shape.Storage;
|
|
var (format, guestFormat) = numericClass switch
|
|
{
|
|
TextureNumericClass.Float => (Format.R32Sfloat, GuestPixelFormat.Bits32Float),
|
|
TextureNumericClass.Uint => (Format.R32Uint, GuestPixelFormat.Bits32UInt),
|
|
TextureNumericClass.Sint => (Format.R32Sint, GuestPixelFormat.Bits32SInt),
|
|
_ => throw SubmissionScheduler.Fatal($"A null image needs a supported numeric class: class={numericClass}."),
|
|
};
|
|
// Depth-reference sampling needs a depth view even for the placeholder.
|
|
var depthCompare = shape.DepthCompare && numericClass == TextureNumericClass.Float && !storage;
|
|
if (depthCompare)
|
|
{
|
|
format = Format.D32Sfloat;
|
|
}
|
|
|
|
var description = ImageDescription.Create();
|
|
description.PixelFormat = format;
|
|
description.GuestFormat = guestFormat;
|
|
description.Type = shape.Volume
|
|
? GuestImageType.Color3D
|
|
: shape.OneDimensional ? GuestImageType.Color1D : GuestImageType.Color2D;
|
|
description.Extent = new Extent3D(1, 1, 1);
|
|
description.Resources = SubresourceCount.Single;
|
|
description.BytesPerBlock = 4;
|
|
description.Samples = shape.Multisampled ? 4u : 1u;
|
|
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = 0, Pitch = 1, Height = 1 };
|
|
var view = ImageViewDescription.Default with
|
|
{
|
|
Format = format,
|
|
Type = shape.Volume
|
|
? ImageViewType.Type3D
|
|
: shape.OneDimensional
|
|
? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D
|
|
: shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D,
|
|
Aspect = depthCompare ? ImageAspectFlags.DepthBit : ImageAspectFlags.ColorBit,
|
|
Usage = storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit,
|
|
};
|
|
return new ImageRequest(description, view, storage ? ImageRole.StorageImage : ImageRole.Texture);
|
|
}
|
|
|
|
// A depth placeholder reads its value into red, as sampled depth views do.
|
|
private static TextureRequestResolution NullTextureResolution(in ShaderImageShape shape)
|
|
{
|
|
var request = NullTexture(shape);
|
|
var swizzle = request.Description.IsDepth ? ViewFormatRules.PackDestinationSelect(4, 0, 0, 1) : 0u;
|
|
return new TextureRequestResolution(request, false, request.View.Format, swizzle);
|
|
}
|
|
|
|
// Fills the mip layout of a texture from the guest tiling rules.
|
|
public static void PopulateTextureMipLayout(ref ImageDescription description)
|
|
{
|
|
if (description.IsVolume && description.TileMode != GuestTileMode.Linear)
|
|
{
|
|
var surfaceDescription = new TiledSurfaceDescription(
|
|
description.GuestFormat, description.TileMode, TileSurfaceDimension.Volume3D,
|
|
description.Extent.Width, description.Extent.Height, description.Extent.Depth, description.Resources.Levels, 1);
|
|
if (!TileGeometry.TryGetTiledTextureLayout(surfaceDescription, out var surface))
|
|
{
|
|
throw SubmissionScheduler.Fatal(
|
|
$"The volume texture layout is not supported: format={(uint)description.GuestFormat} tile={(uint)description.TileMode} " +
|
|
$"extent={description.Extent.Width}x{description.Extent.Height}x{description.Extent.Depth} levels={description.Resources.Levels}.");
|
|
}
|
|
|
|
for (var level = 0; level < description.Resources.Levels; level++)
|
|
{
|
|
var mip = surface.Mips[level];
|
|
description.MipLayout[level] = new MipLevelLayout { Offset = mip.Offset, Size = mip.Size, Pitch = mip.PaddedWidth, Height = mip.PaddedHeight };
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var spans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];
|
|
var padded = new TilePaddedSize[TiledSurfaceLayout.MaxLevels];
|
|
TileGeometry.TryGetTextureSize(
|
|
description.GuestFormat, description.Extent.Width, description.Extent.Height, description.Resources.Levels, description.TileMode,
|
|
out _, spans, padded);
|
|
for (var level = 0; level < description.Resources.Levels; level++)
|
|
{
|
|
var span = spans[level];
|
|
var offset = span.SourceSize != 0 ? span.SourceOffset : span.Offset;
|
|
ulong size = span.SourceSize != 0 ? span.SourceSize : span.Size;
|
|
size *= description.IsVolume ? Math.Max(description.Extent.Depth >> level, 1) : description.Resources.Layers;
|
|
description.MipLayout[level] = new MipLevelLayout
|
|
{
|
|
Offset = offset,
|
|
Size = size,
|
|
Pitch = padded[level].Width != 0 ? padded[level].Width : Math.Max(description.Pitch >> level, 1),
|
|
Height = padded[level].Height != 0 ? padded[level].Height : Math.Max(description.Extent.Height >> level, 1),
|
|
};
|
|
}
|
|
}
|
|
|
|
// The view follows the compiled module: a volume, a layer window to the last layer, or one layer.
|
|
private static ImageViewDescription TextureView(
|
|
in TextureDescriptorWords descriptor, in ShaderImageShape shape, Format format, bool shaderConversion, uint viewLevels, uint imageLayers)
|
|
{
|
|
var mapping = shape.Storage || shaderConversion ? default : ViewFormatRules.ComponentMapping(DestinationSwizzle(descriptor));
|
|
var usage = shape.Storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit;
|
|
if (shape.Volume)
|
|
{
|
|
return WithMinLod(new ImageViewDescription(format, ImageViewType.Type3D, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, 0, 1, mapping, usage), descriptor, shape);
|
|
}
|
|
|
|
var baseLayer = descriptor.BaseArray;
|
|
if (baseLayer >= imageLayers)
|
|
{
|
|
throw SubmissionScheduler.Fatal($"The texture base layer is outside the image: baseLayer={baseLayer} layers={imageLayers} address=0x{descriptor.BaseAddress:X16}.");
|
|
}
|
|
|
|
var layerCount = shape.Arrayed ? imageLayers - baseLayer : 1;
|
|
var type = shape.OneDimensional
|
|
? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D
|
|
: shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D;
|
|
return WithMinLod(new ImageViewDescription(format, type, ImageAspectFlags.ColorBit, descriptor.BaseLevel, viewLevels, baseLayer, layerCount, mapping, usage), descriptor, shape);
|
|
}
|
|
|
|
// MIN_LOD (4.8 fixed point, absolute mip levels) keeps sampling off mips a streamed texture has not
|
|
// loaded yet; ignoring it sampled the unloaded mip 0 of Astro Bot's stadium sky and bloom spread its
|
|
// garbage HDR values over the screen. Storage views address levels explicitly and are not clamped.
|
|
private static ImageViewDescription WithMinLod(ImageViewDescription view, in TextureDescriptorWords descriptor, in ShaderImageShape shape)
|
|
{
|
|
var minLod = descriptor.MinLod / 256f;
|
|
if (shape.Storage || minLod <= view.BaseLevel)
|
|
{
|
|
return view;
|
|
}
|
|
|
|
return view with { MinLod = Math.Min(minLod, view.BaseLevel + view.LevelCount - 1) };
|
|
}
|
|
|
|
public static uint DestinationSwizzle(in TextureDescriptorWords descriptor) =>
|
|
ViewFormatRules.PackDestinationSelect(descriptor.DestinationSelectX, descriptor.DestinationSelectY, descriptor.DestinationSelectZ, descriptor.DestinationSelectW);
|
|
|
|
// Cube maps become two-dimensional arrays; the multisample kinds keep their own type.
|
|
private static GuestImageType TextureType(GuestImageType type) => type == GuestImageType.Cube ? GuestImageType.Color2DArray : type;
|
|
|
|
private static GuestImageType TextureBaseType(GuestImageType type) => type switch
|
|
{
|
|
GuestImageType.Color1DArray => GuestImageType.Color1D,
|
|
GuestImageType.Color2DArray or GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray => GuestImageType.Color2D,
|
|
_ => type,
|
|
};
|
|
|
|
private static bool IsMultisampledTexture(GuestImageType type) => type is GuestImageType.Color2DMsaa or GuestImageType.Color2DMsaaArray;
|
|
|
|
private readonly record struct TextureKey(uint W0, uint W1, uint W2, uint W3, uint W4, uint W5, uint W6, uint W7, ShaderImageShape Shape);
|
|
|
|
private const int TextureCacheLimit = 8192;
|
|
|
|
// Every texture binding of every draw resolves its T# here, and the tiled layout math
|
|
// allocates several arrays each time; the result depends only on the words and shape
|
|
// and is a pure value, so it is memoised per thread.
|
|
[ThreadStatic]
|
|
private static Dictionary<TextureKey, TextureRequestResolution>? _textureCache;
|
|
|
|
// Builds the request for a sampled or storage texture; the words are the eight T# dwords.
|
|
public static TextureRequestResolution Texture(ReadOnlySpan<uint> words, in ShaderImageShape shape)
|
|
{
|
|
Span<uint> key = stackalloc uint[8];
|
|
words[..Math.Min(words.Length, 8)].CopyTo(key);
|
|
var cacheKey = new TextureKey(key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7], shape);
|
|
var cache = _textureCache ??= new Dictionary<TextureKey, TextureRequestResolution>();
|
|
if (cache.TryGetValue(cacheKey, out var cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
var resolution = BuildTexture(key, shape);
|
|
if (cache.Count >= TextureCacheLimit)
|
|
{
|
|
cache.Clear();
|
|
}
|
|
|
|
cache[cacheKey] = resolution;
|
|
return resolution;
|
|
}
|
|
|
|
private static TextureRequestResolution BuildTexture(ReadOnlySpan<uint> words, in ShaderImageShape shape)
|
|
{
|
|
Span<uint> padded = stackalloc uint[8];
|
|
words[..Math.Min(words.Length, 8)].CopyTo(padded);
|
|
var descriptor = new TextureDescriptorWords(padded);
|
|
var storage = shape.Storage;
|
|
if (descriptor.BaseAddress == 0)
|
|
{
|
|
return NullTextureResolution(shape);
|
|
}
|
|
|
|
var address = descriptor.BaseAddress;
|
|
var width = descriptor.Width + 1;
|
|
var height = descriptor.Height + 1;
|
|
var baseLevel = descriptor.BaseLevel;
|
|
var lastLevel = descriptor.LastLevel;
|
|
var type = TextureType(descriptor.Type);
|
|
var multisampled = IsMultisampledTexture(type);
|
|
var maxMip = shape.R128 ? lastLevel : descriptor.MaxMip;
|
|
var levels = multisampled ? 1 : maxMip + 1;
|
|
var dynamicStorage = storage && shape.DynamicMip;
|
|
var viewLastLevel = !multisampled && !dynamicStorage ? Math.Min(lastLevel, maxMip) : lastLevel;
|
|
var tile = descriptor.TileMode;
|
|
var depthTile = tile == GuestTileMode.Depth;
|
|
var msaaTile = depthTile || tile == GuestTileMode.RenderTarget;
|
|
var msaaArray = type == GuestImageType.Color2DMsaaArray;
|
|
|
|
if (!multisampled && baseLevel >= levels)
|
|
{
|
|
return NullTextureResolution(shape);
|
|
}
|
|
|
|
if ((!multisampled && (baseLevel > viewLastLevel || viewLastLevel >= levels)) ||
|
|
(multisampled &&
|
|
(baseLevel != 0 || lastLevel == 0 || lastLevel > 3 || maxMip != lastLevel || !msaaTile || (descriptor.MsaaDepth && !depthTile) ||
|
|
(!msaaArray && (descriptor.Depth != 0 || descriptor.BaseArray != 0)))))
|
|
{
|
|
throw SubmissionScheduler.Fatal(
|
|
$"The texture mip view is not supported: address=0x{address:X16} type={(uint)type} baseLevel={baseLevel} lastLevel={lastLevel} maxMip={maxMip} " +
|
|
$"levels={levels} tile={(uint)tile} depth={descriptor.Depth} baseArray={descriptor.BaseArray} msaaDepth={descriptor.MsaaDepth} storage={storage}.");
|
|
}
|
|
|
|
var samples = multisampled ? 1u << (int)lastLevel : 1u;
|
|
var viewLevels = multisampled ? 1 : viewLastLevel - baseLevel + 1;
|
|
var depth = descriptor.Depth + 1;
|
|
var format = descriptor.Format;
|
|
var surfaceFormat = TextureTransferLayout.SurfaceFormat(format);
|
|
var shaderConversion = surfaceFormat.ConversionFormat != GuestPixelFormat.Invalid;
|
|
var volume = type == GuestImageType.Color3D;
|
|
var layered = type is GuestImageType.Color1DArray or GuestImageType.Color2DArray or GuestImageType.Color2DMsaaArray;
|
|
var imageLayers = layered ? depth : 1;
|
|
if (shape.Cube &&
|
|
(volume || multisampled || width != height || descriptor.BaseArray > descriptor.Depth ||
|
|
(descriptor.Depth - descriptor.BaseArray + 1) % 6 != 0))
|
|
{
|
|
throw SubmissionScheduler.Fatal(
|
|
$"The cubemap view is invalid: address=0x{address:X16} extent={width}x{height} layers={imageLayers} baseArray={descriptor.BaseArray} samples={samples}.");
|
|
}
|
|
uint pitch;
|
|
TileSizeAndAlignment size;
|
|
if (multisampled)
|
|
{
|
|
var bytes = GuestPixelFormats.BytesPerElement(format);
|
|
pitch = depthTile ? TileGeometry.DepthPitch(width, bytes, lastLevel) : TileGeometry.RenderTargetPitch(width, bytes, lastLevel);
|
|
if (pitch == 0 || !TileGeometry.TryGetRenderTargetSize(width, height, pitch, bytes, out size, lastLevel) || size.Size > uint.MaxValue / imageLayers)
|
|
{
|
|
throw SubmissionScheduler.Fatal(
|
|
$"The multisample texture layout is not supported: address=0x{address:X16} extent={width}x{height} bytes={bytes} pitch={pitch} samplesLog2={lastLevel} layers={imageLayers}.");
|
|
}
|
|
|
|
size = new TileSizeAndAlignment(size.Size * imageLayers, size.Align);
|
|
}
|
|
else
|
|
{
|
|
pitch = TileGeometry.TexturePitch(format, width, tile);
|
|
size = TileGeometry.TextureTotalSize(format, width, height, volume ? depth : imageLayers, levels, tile, volume);
|
|
}
|
|
|
|
if (size.Size == 0 || size.Align == 0 || (address & (size.Align - 1UL)) != 0)
|
|
{
|
|
throw SubmissionScheduler.Fatal(
|
|
$"The texture footprint or alignment is invalid: address=0x{address:X16} size=0x{size.Size:X} align=0x{size.Align:X} format={(uint)format} tile={(uint)tile}.");
|
|
}
|
|
|
|
var pixelFormat = surfaceFormat.HostFormat;
|
|
// Comparison sampling needs a depth image, even when no depth target created it.
|
|
if (shape.DepthCompare && DepthFormatRule.FindByGuestFormat(format) is { } depthFormat)
|
|
{
|
|
pixelFormat = depthFormat.DepthAttachmentFormat;
|
|
}
|
|
// Atomic storage images are declared as UINT in SPIR-V, including float atomics.
|
|
var storageViewFormat = storage && (shape.Atomic || format == GuestPixelFormat.Bits32SInt) ? Format.R32Uint : ViewFormatRules.SrgbStorageFormat(pixelFormat);
|
|
var viewFormat = storage && storageViewFormat != Format.Undefined ? storageViewFormat : pixelFormat;
|
|
var blockBytes = GuestPixelFormats.BlockCompressedBytes(format);
|
|
var description = ImageDescription.Create();
|
|
description.Data = new GuestSpan(address, size.Size);
|
|
description.PixelFormat = pixelFormat;
|
|
description.GuestFormat = format;
|
|
description.Type = TextureBaseType(type);
|
|
description.Extent = new Extent3D(width, height, volume ? depth : 1);
|
|
description.Resources = new SubresourceCount(levels, imageLayers);
|
|
description.Pitch = pitch;
|
|
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
|
|
description.Samples = samples;
|
|
description.TileMode = tile;
|
|
var metadataAddress = descriptor.MetadataAddress << 8;
|
|
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
|
|
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
|
|
{
|
|
description.Metadata.Kind = MetadataKind.Dcc;
|
|
description.Metadata.Range = new GuestSpan(metadataAddress, 0);
|
|
}
|
|
|
|
if (samples > 1)
|
|
{
|
|
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size.Size, Pitch = pitch, Height = height };
|
|
}
|
|
else
|
|
{
|
|
PopulateTextureMipLayout(ref description);
|
|
}
|
|
|
|
var view = TextureView(descriptor, shape, viewFormat, shaderConversion, viewLevels, description.Resources.Layers);
|
|
var request = new ImageRequest(description, view, storage ? ImageRole.StorageImage : ImageRole.Texture);
|
|
return new TextureRequestResolution(request, shaderConversion, pixelFormat, DestinationSwizzle(descriptor));
|
|
}
|
|
|
|
// After the lookup: a stencil association redirects to its depth owner; the view rules run on the owner.
|
|
public static ResourceSlotIdentifier ValidateTextureOwner(GuestImageCache cache, ResourceSlotIdentifier imageIdentifier, in TextureRequestResolution resolution)
|
|
{
|
|
var image = cache.GetImage(imageIdentifier);
|
|
if (image.DepthOwner.IsValid)
|
|
{
|
|
return image.DepthOwner;
|
|
}
|
|
|
|
var storage = resolution.Request.Role == ImageRole.StorageImage;
|
|
if (image.Description.IsDepth)
|
|
{
|
|
if (storage)
|
|
{
|
|
throw SubmissionScheduler.Fatal($"A depth target cannot be bound as a storage image: address=0x{image.Description.Data.Address:X16}.");
|
|
}
|
|
|
|
_ = ViewFormatRules.SelectSampledDepthView(image.Description.PixelFormat, resolution.ViewFormat, resolution.Swizzle);
|
|
}
|
|
else if (storage)
|
|
{
|
|
ViewFormatRules.ValidateStorageColorView(image.Description.PixelFormat, resolution.Request.View.Format, resolution.Swizzle);
|
|
}
|
|
else
|
|
{
|
|
_ = ViewFormatRules.SelectSampledColorView(image.Description.PixelFormat, resolution.ViewFormat, resolution.Swizzle);
|
|
}
|
|
|
|
return imageIdentifier;
|
|
}
|
|
}
|