diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Graphics.Gpu/Image | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Graphics.Gpu/Image')
34 files changed, 10874 insertions, 0 deletions
diff --git a/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs b/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs new file mode 100644 index 00000000..a0b9f57b --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs @@ -0,0 +1,256 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// An entry on the short duration texture cache. + /// </summary> + class ShortTextureCacheEntry + { + public readonly TextureDescriptor Descriptor; + public readonly int InvalidatedSequence; + public readonly Texture Texture; + + /// <summary> + /// Create a new entry on the short duration texture cache. + /// </summary> + /// <param name="descriptor">Last descriptor that referenced the texture</param> + /// <param name="texture">The texture</param> + public ShortTextureCacheEntry(TextureDescriptor descriptor, Texture texture) + { + Descriptor = descriptor; + InvalidatedSequence = texture.InvalidatedSequence; + Texture = texture; + } + } + + /// <summary> + /// A texture cache that automatically removes older textures that are not used for some time. + /// The cache works with a rotated list with a fixed size. When new textures are added, the + /// old ones at the bottom of the list are deleted. + /// </summary> + class AutoDeleteCache : IEnumerable<Texture> + { + private const int MinCountForDeletion = 32; + private const int MaxCapacity = 2048; + private const ulong MaxTextureSizeCapacity = 512 * 1024 * 1024; // MB; + + private readonly LinkedList<Texture> _textures; + private ulong _totalSize; + + private HashSet<ShortTextureCacheEntry> _shortCacheBuilder; + private HashSet<ShortTextureCacheEntry> _shortCache; + + private Dictionary<TextureDescriptor, ShortTextureCacheEntry> _shortCacheLookup; + + /// <summary> + /// Creates a new instance of the automatic deletion cache. + /// </summary> + public AutoDeleteCache() + { + _textures = new LinkedList<Texture>(); + + _shortCacheBuilder = new HashSet<ShortTextureCacheEntry>(); + _shortCache = new HashSet<ShortTextureCacheEntry>(); + + _shortCacheLookup = new Dictionary<TextureDescriptor, ShortTextureCacheEntry>(); + } + + /// <summary> + /// Adds a new texture to the cache, even if the texture added is already on the cache. + /// </summary> + /// <remarks> + /// Using this method is only recommended if you know that the texture is not yet on the cache, + /// otherwise it would store the same texture more than once. + /// </remarks> + /// <param name="texture">The texture to be added to the cache</param> + public void Add(Texture texture) + { + _totalSize += texture.Size; + + texture.IncrementReferenceCount(); + texture.CacheNode = _textures.AddLast(texture); + + if (_textures.Count > MaxCapacity || + (_totalSize > MaxTextureSizeCapacity && _textures.Count >= MinCountForDeletion)) + { + RemoveLeastUsedTexture(); + } + } + + /// <summary> + /// Adds a new texture to the cache, or just moves it to the top of the list if the + /// texture is already on the cache. + /// </summary> + /// <remarks> + /// Moving the texture to the top of the list prevents it from being deleted, + /// as the textures on the bottom of the list are deleted when new ones are added. + /// </remarks> + /// <param name="texture">The texture to be added, or moved to the top</param> + public void Lift(Texture texture) + { + if (texture.CacheNode != null) + { + if (texture.CacheNode != _textures.Last) + { + _textures.Remove(texture.CacheNode); + + texture.CacheNode = _textures.AddLast(texture); + } + + if (_totalSize > MaxTextureSizeCapacity && _textures.Count >= MinCountForDeletion) + { + RemoveLeastUsedTexture(); + } + } + else + { + Add(texture); + } + } + + /// <summary> + /// Removes the least used texture from the cache. + /// </summary> + private void RemoveLeastUsedTexture() + { + Texture oldestTexture = _textures.First.Value; + + _totalSize -= oldestTexture.Size; + + if (!oldestTexture.CheckModified(false)) + { + // The texture must be flushed if it falls out of the auto delete cache. + // Flushes out of the auto delete cache do not trigger write tracking, + // as it is expected that other overlapping textures exist that have more up-to-date contents. + + oldestTexture.Group.SynchronizeDependents(oldestTexture); + oldestTexture.FlushModified(false); + } + + _textures.RemoveFirst(); + + oldestTexture.DecrementReferenceCount(); + oldestTexture.CacheNode = null; + } + + /// <summary> + /// Removes a texture from the cache. + /// </summary> + /// <param name="texture">The texture to be removed from the cache</param> + /// <param name="flush">True to remove the texture if it was on the cache</param> + /// <returns>True if the texture was found and removed, false otherwise</returns> + public bool Remove(Texture texture, bool flush) + { + if (texture.CacheNode == null) + { + return false; + } + + // Remove our reference to this texture. + if (flush) + { + texture.FlushModified(false); + } + + _textures.Remove(texture.CacheNode); + + _totalSize -= texture.Size; + + texture.CacheNode = null; + + return texture.DecrementReferenceCount(); + } + + /// <summary> + /// Attempt to find a texture on the short duration cache. + /// </summary> + /// <param name="descriptor">The texture descriptor</param> + /// <returns>The texture if found, null otherwise</returns> + public Texture FindShortCache(in TextureDescriptor descriptor) + { + if (_shortCacheLookup.Count > 0 && _shortCacheLookup.TryGetValue(descriptor, out var entry)) + { + if (entry.InvalidatedSequence == entry.Texture.InvalidatedSequence) + { + return entry.Texture; + } + else + { + _shortCacheLookup.Remove(descriptor); + } + } + + return null; + } + + /// <summary> + /// Removes a texture from the short duration cache. + /// </summary> + /// <param name="texture">Texture to remove from the short cache</param> + public void RemoveShortCache(Texture texture) + { + bool removed = _shortCache.Remove(texture.ShortCacheEntry); + removed |= _shortCacheBuilder.Remove(texture.ShortCacheEntry); + + if (removed) + { + texture.DecrementReferenceCount(); + + _shortCacheLookup.Remove(texture.ShortCacheEntry.Descriptor); + texture.ShortCacheEntry = null; + } + } + + /// <summary> + /// Adds a texture to the short duration cache. + /// It starts in the builder set, and it is moved into the deletion set on next process. + /// </summary> + /// <param name="texture">Texture to add to the short cache</param> + /// <param name="descriptor">Last used texture descriptor</param> + public void AddShortCache(Texture texture, ref TextureDescriptor descriptor) + { + var entry = new ShortTextureCacheEntry(descriptor, texture); + + _shortCacheBuilder.Add(entry); + _shortCacheLookup.Add(entry.Descriptor, entry); + + texture.ShortCacheEntry = entry; + + texture.IncrementReferenceCount(); + } + + /// <summary> + /// Delete textures from the short duration cache. + /// Moves the builder set to be deleted on next process. + /// </summary> + public void ProcessShortCache() + { + HashSet<ShortTextureCacheEntry> toRemove = _shortCache; + + foreach (var entry in toRemove) + { + entry.Texture.DecrementReferenceCount(); + + _shortCacheLookup.Remove(entry.Descriptor); + entry.Texture.ShortCacheEntry = null; + } + + toRemove.Clear(); + _shortCache = _shortCacheBuilder; + _shortCacheBuilder = toRemove; + } + + public IEnumerator<Texture> GetEnumerator() + { + return _textures.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _textures.GetEnumerator(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs new file mode 100644 index 00000000..9ee649d2 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs @@ -0,0 +1,72 @@ +using Ryujinx.Graphics.GAL; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Represents texture format information. + /// </summary> + readonly struct FormatInfo + { + /// <summary> + /// A default, generic RGBA8 texture format. + /// </summary> + public static FormatInfo Default { get; } = new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); + + /// <summary> + /// The format of the texture data. + /// </summary> + public Format Format { get; } + + /// <summary> + /// The block width for compressed formats. + /// </summary> + /// <remarks> + /// Must be 1 for non-compressed formats. + /// </remarks> + public int BlockWidth { get; } + + /// <summary> + /// The block height for compressed formats. + /// </summary> + /// <remarks> + /// Must be 1 for non-compressed formats. + /// </remarks> + public int BlockHeight { get; } + + /// <summary> + /// The number of bytes occupied by a single pixel in memory of the texture data. + /// </summary> + public int BytesPerPixel { get; } + + /// <summary> + /// The maximum number of components this format has defined (in RGBA order). + /// </summary> + public int Components { get; } + + /// <summary> + /// Whenever or not the texture format is a compressed format. Determined from block size. + /// </summary> + public bool IsCompressed => (BlockWidth | BlockHeight) != 1; + + /// <summary> + /// Constructs the texture format info structure. + /// </summary> + /// <param name="format">The format of the texture data</param> + /// <param name="blockWidth">The block width for compressed formats. Must be 1 for non-compressed formats</param> + /// <param name="blockHeight">The block height for compressed formats. Must be 1 for non-compressed formats</param> + /// <param name="bytesPerPixel">The number of bytes occupied by a single pixel in memory of the texture data</param> + public FormatInfo( + Format format, + int blockWidth, + int blockHeight, + int bytesPerPixel, + int components) + { + Format = format; + BlockWidth = blockWidth; + BlockHeight = blockHeight; + BytesPerPixel = bytesPerPixel; + Components = components; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs new file mode 100644 index 00000000..72901610 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs @@ -0,0 +1,578 @@ +using Ryujinx.Graphics.GAL; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Contains format tables, for texture and vertex attribute formats. + /// </summary> + static class FormatTable + { + private enum TextureFormat : uint + { + // Formats + R32G32B32A32 = 0x01, + R32G32B32 = 0x02, + R16G16B16A16 = 0x03, + R32G32 = 0x04, + R32B24G8 = 0x05, + X8B8G8R8 = 0x07, + A8B8G8R8 = 0x08, + A2B10G10R10 = 0x09, + R16G16 = 0x0c, + G8R24 = 0x0d, + G24R8 = 0x0e, + R32 = 0x0f, + A4B4G4R4 = 0x12, + A5B5G5R1 = 0x13, + A1B5G5R5 = 0x14, + B5G6R5 = 0x15, + B6G5R5 = 0x16, + G8R8 = 0x18, + R16 = 0x1b, + Y8Video = 0x1c, + R8 = 0x1d, + G4R4 = 0x1e, + R1 = 0x1f, + E5B9G9R9SharedExp = 0x20, + Bf10Gf11Rf11 = 0x21, + G8B8G8R8 = 0x22, + B8G8R8G8 = 0x23, + Bc1 = 0x24, + Bc2 = 0x25, + Bc3 = 0x26, + Bc4 = 0x27, + Bc5 = 0x28, + Bc6HSf16 = 0x10, + Bc6HUf16 = 0x11, + Bc7U = 0x17, + Etc2Rgb = 0x06, + Etc2RgbPta = 0x0a, + Etc2Rgba = 0x0b, + Eac = 0x19, + Eacx2 = 0x1a, + Z24S8 = 0x29, + X8Z24 = 0x2a, + S8Z24 = 0x2b, + X4V4Z24Cov4R4V = 0x2c, + X4V4Z24Cov8R8V = 0x2d, + V8Z24Cov4R12V = 0x2e, + Zf32 = 0x2f, + Zf32X24S8 = 0x30, + X8Z24X20V4S8Cov4R4V = 0x31, + X8Z24X20V4S8Cov8R8V = 0x32, + Zf32X20V4X8Cov4R4V = 0x33, + Zf32X20V4X8Cov8R8V = 0x34, + Zf32X20V4S8Cov4R4V = 0x35, + Zf32X20V4S8Cov8R8V = 0x36, + X8Z24X16V8S8Cov4R12V = 0x37, + Zf32X16V8X8Cov4R12V = 0x38, + Zf32X16V8S8Cov4R12V = 0x39, + Z16 = 0x3a, + V8Z24Cov8R24V = 0x3b, + X8Z24X16V8S8Cov8R24V = 0x3c, + Zf32X16V8X8Cov8R24V = 0x3d, + Zf32X16V8S8Cov8R24V = 0x3e, + Astc2D4x4 = 0x40, + Astc2D5x4 = 0x50, + Astc2D5x5 = 0x41, + Astc2D6x5 = 0x51, + Astc2D6x6 = 0x42, + Astc2D8x5 = 0x55, + Astc2D8x6 = 0x52, + Astc2D8x8 = 0x44, + Astc2D10x5 = 0x56, + Astc2D10x6 = 0x57, + Astc2D10x8 = 0x53, + Astc2D10x10 = 0x45, + Astc2D12x10 = 0x54, + Astc2D12x12 = 0x46, + + // Types + Snorm = 0x1, + Unorm = 0x2, + Sint = 0x3, + Uint = 0x4, + SnormForceFp16 = 0x5, + UnormForceFp16 = 0x6, + Float = 0x7, + + // Component Types + RSnorm = Snorm << 7, + GSnorm = Snorm << 10, + BSnorm = Snorm << 13, + ASnorm = Snorm << 16, + + RUnorm = Unorm << 7, + GUnorm = Unorm << 10, + BUnorm = Unorm << 13, + AUnorm = Unorm << 16, + + RSint = Sint << 7, + GSint = Sint << 10, + BSint = Sint << 13, + ASint = Sint << 16, + + RUint = Uint << 7, + GUint = Uint << 10, + BUint = Uint << 13, + AUint = Uint << 16, + + RSnormForceFp16 = SnormForceFp16 << 7, + GSnormForceFp16 = SnormForceFp16 << 10, + BSnormForceFp16 = SnormForceFp16 << 13, + ASnormForceFp16 = SnormForceFp16 << 16, + + RUnormForceFp16 = UnormForceFp16 << 7, + GUnormForceFp16 = UnormForceFp16 << 10, + BUnormForceFp16 = UnormForceFp16 << 13, + AUnormForceFp16 = UnormForceFp16 << 16, + + RFloat = Float << 7, + GFloat = Float << 10, + BFloat = Float << 13, + AFloat = Float << 16, + + Srgb = 0x1 << 19, // Custom encoding + + // Combinations + R8Unorm = R8 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2491d + R8Snorm = R8 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x1249d + R8Uint = R8 | RUint | GUint | BUint | AUint, // 0x4921d + R8Sint = R8 | RSint | GSint | BSint | ASint, // 0x36d9d + R16Float = R16 | RFloat | GFloat | BFloat | AFloat, // 0x7ff9b + R16Unorm = R16 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2491b + R16Snorm = R16 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x1249b + R16Uint = R16 | RUint | GUint | BUint | AUint, // 0x4921b + R16Sint = R16 | RSint | GSint | BSint | ASint, // 0x36d9b + R32Float = R32 | RFloat | GFloat | BFloat | AFloat, // 0x7ff8f + R32Uint = R32 | RUint | GUint | BUint | AUint, // 0x4920f + R32Sint = R32 | RSint | GSint | BSint | ASint, // 0x36d8f + G8R8Unorm = G8R8 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24918 + G8R8Snorm = G8R8 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x12498 + G8R8Uint = G8R8 | RUint | GUint | BUint | AUint, // 0x49218 + G8R8Sint = G8R8 | RSint | GSint | BSint | ASint, // 0x36d98 + R16G16Float = R16G16 | RFloat | GFloat | BFloat | AFloat, // 0x7ff8c + R16G16Unorm = R16G16 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2490c + R16G16Snorm = R16G16 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x1248c + R16G16Uint = R16G16 | RUint | GUint | BUint | AUint, // 0x4920c + R16G16Sint = R16G16 | RSint | GSint | BSint | ASint, // 0x36d8c + R32G32Float = R32G32 | RFloat | GFloat | BFloat | AFloat, // 0x7ff84 + R32G32Uint = R32G32 | RUint | GUint | BUint | AUint, // 0x49204 + R32G32Sint = R32G32 | RSint | GSint | BSint | ASint, // 0x36d84 + R32G32B32Float = R32G32B32 | RFloat | GFloat | BFloat | AFloat, // 0x7ff82 + R32G32B32Uint = R32G32B32 | RUint | GUint | BUint | AUint, // 0x49202 + R32G32B32Sint = R32G32B32 | RSint | GSint | BSint | ASint, // 0x36d82 + A8B8G8R8Unorm = A8B8G8R8 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24908 + A8B8G8R8Snorm = A8B8G8R8 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x12488 + A8B8G8R8Uint = A8B8G8R8 | RUint | GUint | BUint | AUint, // 0x49208 + A8B8G8R8Sint = A8B8G8R8 | RSint | GSint | BSint | ASint, // 0x36d88 + R16G16B16A16Float = R16G16B16A16 | RFloat | GFloat | BFloat | AFloat, // 0x7ff83 + R16G16B16A16Unorm = R16G16B16A16 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24903 + R16G16B16A16Snorm = R16G16B16A16 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x12483 + R16G16B16A16Uint = R16G16B16A16 | RUint | GUint | BUint | AUint, // 0x49203 + R16G16B16A16Sint = R16G16B16A16 | RSint | GSint | BSint | ASint, // 0x36d83 + R32G32B32A32Float = R32G32B32A32 | RFloat | GFloat | BFloat | AFloat, // 0x7ff81 + R32G32B32A32Uint = R32G32B32A32 | RUint | GUint | BUint | AUint, // 0x49201 + R32G32B32A32Sint = R32G32B32A32 | RSint | GSint | BSint | ASint, // 0x36d81 + Z16Unorm = Z16 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2493a + Zf32RFloatGUintBUintAUint = Zf32 | RFloat | GUint | BUint | AUint, // 0x493af + Zf32Float = Zf32 | RFloat | GFloat | BFloat | AFloat, // 0x7ffaf + G24R8RUintGUnormBUnormAUnorm = G24R8 | RUint | GUnorm | BUnorm | AUnorm, // 0x24a0e + Z24S8RUintGUnormBUnormAUnorm = Z24S8 | RUint | GUnorm | BUnorm | AUnorm, // 0x24a29 + Z24S8RUintGUnormBUintAUint = Z24S8 | RUint | GUnorm | BUint | AUint, // 0x48a29 + S8Z24RUnormGUintBUintAUint = S8Z24 | RUnorm | GUint | BUint | AUint, // 0x4912b + R32B24G8RFloatGUintBUnormAUnorm = R32B24G8 | RFloat | GUint | BUnorm | AUnorm, // 0x25385 + Zf32X24S8RFloatGUintBUnormAUnorm = Zf32X24S8 | RFloat | GUint | BUnorm | AUnorm, // 0x253b0 + A8B8G8R8UnormSrgb = A8B8G8R8 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4908 + G4R4Unorm = G4R4 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2491e + A4B4G4R4Unorm = A4B4G4R4 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24912 + A1B5G5R5Unorm = A1B5G5R5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24914 + B5G6R5Unorm = B5G6R5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24915 + A2B10G10R10Unorm = A2B10G10R10 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24909 + A2B10G10R10Uint = A2B10G10R10 | RUint | GUint | BUint | AUint, // 0x49209 + Bf10Gf11Rf11Float = Bf10Gf11Rf11 | RFloat | GFloat | BFloat | AFloat, // 0x7ffa1 + E5B9G9R9SharedExpFloat = E5B9G9R9SharedExp | RFloat | GFloat | BFloat | AFloat, // 0x7ffa0 + Bc1Unorm = Bc1 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24924 + Bc2Unorm = Bc2 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24925 + Bc3Unorm = Bc3 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24926 + Bc1UnormSrgb = Bc1 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4924 + Bc2UnormSrgb = Bc2 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4925 + Bc3UnormSrgb = Bc3 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4926 + Bc4Unorm = Bc4 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24927 + Bc4Snorm = Bc4 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x124a7 + Bc5Unorm = Bc5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24928 + Bc5Snorm = Bc5 | RSnorm | GSnorm | BSnorm | ASnorm, // 0x124a8 + Bc7UUnorm = Bc7U | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24917 + Bc7UUnormSrgb = Bc7U | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4917 + Bc6HSf16Float = Bc6HSf16 | RFloat | GFloat | BFloat | AFloat, // 0x7ff90 + Bc6HUf16Float = Bc6HUf16 | RFloat | GFloat | BFloat | AFloat, // 0x7ff91 + Etc2RgbUnorm = Etc2Rgb | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24906 + Etc2RgbPtaUnorm = Etc2RgbPta | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2490a + Etc2RgbaUnorm = Etc2Rgba | RUnorm | GUnorm | BUnorm | AUnorm, // 0x2490b + Etc2RgbUnormSrgb = Etc2Rgb | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4906 + Etc2RgbPtaUnormSrgb = Etc2RgbPta | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa490a + Etc2RgbaUnormSrgb = Etc2Rgba | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa490b + Astc2D4x4Unorm = Astc2D4x4 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24940 + Astc2D5x4Unorm = Astc2D5x4 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24950 + Astc2D5x5Unorm = Astc2D5x5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24941 + Astc2D6x5Unorm = Astc2D6x5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24951 + Astc2D6x6Unorm = Astc2D6x6 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24942 + Astc2D8x5Unorm = Astc2D8x5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24955 + Astc2D8x6Unorm = Astc2D8x6 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24952 + Astc2D8x8Unorm = Astc2D8x8 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24944 + Astc2D10x5Unorm = Astc2D10x5 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24956 + Astc2D10x6Unorm = Astc2D10x6 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24957 + Astc2D10x8Unorm = Astc2D10x8 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24953 + Astc2D10x10Unorm = Astc2D10x10 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24945 + Astc2D12x10Unorm = Astc2D12x10 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24954 + Astc2D12x12Unorm = Astc2D12x12 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24946 + Astc2D4x4UnormSrgb = Astc2D4x4 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4940 + Astc2D5x4UnormSrgb = Astc2D5x4 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4950 + Astc2D5x5UnormSrgb = Astc2D5x5 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4941 + Astc2D6x5UnormSrgb = Astc2D6x5 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4951 + Astc2D6x6UnormSrgb = Astc2D6x6 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4942 + Astc2D8x5UnormSrgb = Astc2D8x5 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4955 + Astc2D8x6UnormSrgb = Astc2D8x6 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4952 + Astc2D8x8UnormSrgb = Astc2D8x8 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4944 + Astc2D10x5UnormSrgb = Astc2D10x5 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4956 + Astc2D10x6UnormSrgb = Astc2D10x6 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4957 + Astc2D10x8UnormSrgb = Astc2D10x8 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4953 + Astc2D10x10UnormSrgb = Astc2D10x10 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4945 + Astc2D12x10UnormSrgb = Astc2D12x10 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4954 + Astc2D12x12UnormSrgb = Astc2D12x12 | RUnorm | GUnorm | BUnorm | AUnorm | Srgb, // 0xa4946 + A5B5G5R1Unorm = A5B5G5R1 | RUnorm | GUnorm | BUnorm | AUnorm, // 0x24913 + } + + private enum VertexAttributeFormat : uint + { + // Width + R32G32B32A32 = 0x01, + R32G32B32 = 0x02, + R16G16B16A16 = 0x03, + R32G32 = 0x04, + R16G16B16 = 0x05, + A8B8G8R8 = 0x2f, + R8G8B8A8 = 0x0a, + X8B8G8R8 = 0x33, + A2B10G10R10 = 0x30, + B10G11R11 = 0x31, + R16G16 = 0x0f, + R32 = 0x12, + R8G8B8 = 0x13, + G8R8 = 0x32, + R8G8 = 0x18, + R16 = 0x1b, + R8 = 0x1d, + A8 = 0x34, + + // Type + Snorm = 0x01, + Unorm = 0x02, + Sint = 0x03, + Uint = 0x04, + Uscaled = 0x05, + Sscaled = 0x06, + Float = 0x07, + + // Combinations + R8Unorm = (R8 << 21) | (Unorm << 27), // 0x13a00000 + R8Snorm = (R8 << 21) | (Snorm << 27), // 0x0ba00000 + R8Uint = (R8 << 21) | (Uint << 27), // 0x23a00000 + R8Sint = (R8 << 21) | (Sint << 27), // 0x1ba00000 + R16Float = (R16 << 21) | (Float << 27), // 0x3b600000 + R16Unorm = (R16 << 21) | (Unorm << 27), // 0x13600000 + R16Snorm = (R16 << 21) | (Snorm << 27), // 0x0b600000 + R16Uint = (R16 << 21) | (Uint << 27), // 0x23600000 + R16Sint = (R16 << 21) | (Sint << 27), // 0x1b600000 + R32Float = (R32 << 21) | (Float << 27), // 0x3a400000 + R32Uint = (R32 << 21) | (Uint << 27), // 0x22400000 + R32Sint = (R32 << 21) | (Sint << 27), // 0x1a400000 + R8G8Unorm = (R8G8 << 21) | (Unorm << 27), // 0x13000000 + R8G8Snorm = (R8G8 << 21) | (Snorm << 27), // 0x0b000000 + R8G8Uint = (R8G8 << 21) | (Uint << 27), // 0x23000000 + R8G8Sint = (R8G8 << 21) | (Sint << 27), // 0x1b000000 + R16G16Float = (R16G16 << 21) | (Float << 27), // 0x39e00000 + R16G16Unorm = (R16G16 << 21) | (Unorm << 27), // 0x11e00000 + R16G16Snorm = (R16G16 << 21) | (Snorm << 27), // 0x09e00000 + R16G16Uint = (R16G16 << 21) | (Uint << 27), // 0x21e00000 + R16G16Sint = (R16G16 << 21) | (Sint << 27), // 0x19e00000 + R32G32Float = (R32G32 << 21) | (Float << 27), // 0x38800000 + R32G32Uint = (R32G32 << 21) | (Uint << 27), // 0x20800000 + R32G32Sint = (R32G32 << 21) | (Sint << 27), // 0x18800000 + R8G8B8Unorm = (R8G8B8 << 21) | (Unorm << 27), // 0x12600000 + R8G8B8Snorm = (R8G8B8 << 21) | (Snorm << 27), // 0x0a600000 + R8G8B8Uint = (R8G8B8 << 21) | (Uint << 27), // 0x22600000 + R8G8B8Sint = (R8G8B8 << 21) | (Sint << 27), // 0x1a600000 + R16G16B16Float = (R16G16B16 << 21) | (Float << 27), // 0x38a00000 + R16G16B16Unorm = (R16G16B16 << 21) | (Unorm << 27), // 0x10a00000 + R16G16B16Snorm = (R16G16B16 << 21) | (Snorm << 27), // 0x08a00000 + R16G16B16Uint = (R16G16B16 << 21) | (Uint << 27), // 0x20a00000 + R16G16B16Sint = (R16G16B16 << 21) | (Sint << 27), // 0x18a00000 + R32G32B32Float = (R32G32B32 << 21) | (Float << 27), // 0x38400000 + R32G32B32Uint = (R32G32B32 << 21) | (Uint << 27), // 0x20400000 + R32G32B32Sint = (R32G32B32 << 21) | (Sint << 27), // 0x18400000 + R8G8B8A8Unorm = (R8G8B8A8 << 21) | (Unorm << 27), // 0x11400000 + R8G8B8A8Snorm = (R8G8B8A8 << 21) | (Snorm << 27), // 0x09400000 + R8G8B8A8Uint = (R8G8B8A8 << 21) | (Uint << 27), // 0x21400000 + R8G8B8A8Sint = (R8G8B8A8 << 21) | (Sint << 27), // 0x19400000 + R16G16B16A16Float = (R16G16B16A16 << 21) | (Float << 27), // 0x38600000 + R16G16B16A16Unorm = (R16G16B16A16 << 21) | (Unorm << 27), // 0x10600000 + R16G16B16A16Snorm = (R16G16B16A16 << 21) | (Snorm << 27), // 0x08600000 + R16G16B16A16Uint = (R16G16B16A16 << 21) | (Uint << 27), // 0x20600000 + R16G16B16A16Sint = (R16G16B16A16 << 21) | (Sint << 27), // 0x18600000 + R32G32B32A32Float = (R32G32B32A32 << 21) | (Float << 27), // 0x38200000 + R32G32B32A32Uint = (R32G32B32A32 << 21) | (Uint << 27), // 0x20200000 + R32G32B32A32Sint = (R32G32B32A32 << 21) | (Sint << 27), // 0x18200000 + A2B10G10R10Unorm = (A2B10G10R10 << 21) | (Unorm << 27), // 0x16000000 + A2B10G10R10Uint = (A2B10G10R10 << 21) | (Uint << 27), // 0x26000000 + B10G11R11Float = (B10G11R11 << 21) | (Float << 27), // 0x3e200000 + R8Uscaled = (R8 << 21) | (Uscaled << 27), // 0x2ba00000 + R8Sscaled = (R8 << 21) | (Sscaled << 27), // 0x33a00000 + R16Uscaled = (R16 << 21) | (Uscaled << 27), // 0x2b600000 + R16Sscaled = (R16 << 21) | (Sscaled << 27), // 0x33600000 + R32Uscaled = (R32 << 21) | (Uscaled << 27), // 0x2a400000 + R32Sscaled = (R32 << 21) | (Sscaled << 27), // 0x32400000 + R8G8Uscaled = (R8G8 << 21) | (Uscaled << 27), // 0x2b000000 + R8G8Sscaled = (R8G8 << 21) | (Sscaled << 27), // 0x33000000 + R16G16Uscaled = (R16G16 << 21) | (Uscaled << 27), // 0x29e00000 + R16G16Sscaled = (R16G16 << 21) | (Sscaled << 27), // 0x31e00000 + R32G32Uscaled = (R32G32 << 21) | (Uscaled << 27), // 0x28800000 + R32G32Sscaled = (R32G32 << 21) | (Sscaled << 27), // 0x30800000 + R8G8B8Uscaled = (R8G8B8 << 21) | (Uscaled << 27), // 0x2a600000 + R8G8B8Sscaled = (R8G8B8 << 21) | (Sscaled << 27), // 0x32600000 + R16G16B16Uscaled = (R16G16B16 << 21) | (Uscaled << 27), // 0x28a00000 + R16G16B16Sscaled = (R16G16B16 << 21) | (Sscaled << 27), // 0x30a00000 + R32G32B32Uscaled = (R32G32B32 << 21) | (Uscaled << 27), // 0x28400000 + R32G32B32Sscaled = (R32G32B32 << 21) | (Sscaled << 27), // 0x30400000 + R8G8B8A8Uscaled = (R8G8B8A8 << 21) | (Uscaled << 27), // 0x29400000 + R8G8B8A8Sscaled = (R8G8B8A8 << 21) | (Sscaled << 27), // 0x31400000 + R16G16B16A16Uscaled = (R16G16B16A16 << 21) | (Uscaled << 27), // 0x28600000 + R16G16B16A16Sscaled = (R16G16B16A16 << 21) | (Sscaled << 27), // 0x30600000 + R32G32B32A32Uscaled = (R32G32B32A32 << 21) | (Uscaled << 27), // 0x28200000 + R32G32B32A32Sscaled = (R32G32B32A32 << 21) | (Sscaled << 27), // 0x30200000 + A2B10G10R10Snorm = (A2B10G10R10 << 21) | (Snorm << 27), // 0x0e000000 + A2B10G10R10Sint = (A2B10G10R10 << 21) | (Sint << 27), // 0x1e000000 + A2B10G10R10Uscaled = (A2B10G10R10 << 21) | (Uscaled << 27), // 0x2e000000 + A2B10G10R10Sscaled = (A2B10G10R10 << 21) | (Sscaled << 27), // 0x36000000 + } + + private static readonly Dictionary<TextureFormat, FormatInfo> _textureFormats = new Dictionary<TextureFormat, FormatInfo>() + { + { TextureFormat.R8Unorm, new FormatInfo(Format.R8Unorm, 1, 1, 1, 1) }, + { TextureFormat.R8Snorm, new FormatInfo(Format.R8Snorm, 1, 1, 1, 1) }, + { TextureFormat.R8Uint, new FormatInfo(Format.R8Uint, 1, 1, 1, 1) }, + { TextureFormat.R8Sint, new FormatInfo(Format.R8Sint, 1, 1, 1, 1) }, + { TextureFormat.R16Float, new FormatInfo(Format.R16Float, 1, 1, 2, 1) }, + { TextureFormat.R16Unorm, new FormatInfo(Format.R16Unorm, 1, 1, 2, 1) }, + { TextureFormat.R16Snorm, new FormatInfo(Format.R16Snorm, 1, 1, 2, 1) }, + { TextureFormat.R16Uint, new FormatInfo(Format.R16Uint, 1, 1, 2, 1) }, + { TextureFormat.R16Sint, new FormatInfo(Format.R16Sint, 1, 1, 2, 1) }, + { TextureFormat.R32Float, new FormatInfo(Format.R32Float, 1, 1, 4, 1) }, + { TextureFormat.R32Uint, new FormatInfo(Format.R32Uint, 1, 1, 4, 1) }, + { TextureFormat.R32Sint, new FormatInfo(Format.R32Sint, 1, 1, 4, 1) }, + { TextureFormat.G8R8Unorm, new FormatInfo(Format.R8G8Unorm, 1, 1, 2, 2) }, + { TextureFormat.G8R8Snorm, new FormatInfo(Format.R8G8Snorm, 1, 1, 2, 2) }, + { TextureFormat.G8R8Uint, new FormatInfo(Format.R8G8Uint, 1, 1, 2, 2) }, + { TextureFormat.G8R8Sint, new FormatInfo(Format.R8G8Sint, 1, 1, 2, 2) }, + { TextureFormat.R16G16Float, new FormatInfo(Format.R16G16Float, 1, 1, 4, 2) }, + { TextureFormat.R16G16Unorm, new FormatInfo(Format.R16G16Unorm, 1, 1, 4, 2) }, + { TextureFormat.R16G16Snorm, new FormatInfo(Format.R16G16Snorm, 1, 1, 4, 2) }, + { TextureFormat.R16G16Uint, new FormatInfo(Format.R16G16Uint, 1, 1, 4, 2) }, + { TextureFormat.R16G16Sint, new FormatInfo(Format.R16G16Sint, 1, 1, 4, 2) }, + { TextureFormat.R32G32Float, new FormatInfo(Format.R32G32Float, 1, 1, 8, 2) }, + { TextureFormat.R32G32Uint, new FormatInfo(Format.R32G32Uint, 1, 1, 8, 2) }, + { TextureFormat.R32G32Sint, new FormatInfo(Format.R32G32Sint, 1, 1, 8, 2) }, + { TextureFormat.R32G32B32Float, new FormatInfo(Format.R32G32B32Float, 1, 1, 12, 3) }, + { TextureFormat.R32G32B32Uint, new FormatInfo(Format.R32G32B32Uint, 1, 1, 12, 3) }, + { TextureFormat.R32G32B32Sint, new FormatInfo(Format.R32G32B32Sint, 1, 1, 12, 3) }, + { TextureFormat.A8B8G8R8Unorm, new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4) }, + { TextureFormat.A8B8G8R8Snorm, new FormatInfo(Format.R8G8B8A8Snorm, 1, 1, 4, 4) }, + { TextureFormat.A8B8G8R8Uint, new FormatInfo(Format.R8G8B8A8Uint, 1, 1, 4, 4) }, + { TextureFormat.A8B8G8R8Sint, new FormatInfo(Format.R8G8B8A8Sint, 1, 1, 4, 4) }, + { TextureFormat.R16G16B16A16Float, new FormatInfo(Format.R16G16B16A16Float, 1, 1, 8, 4) }, + { TextureFormat.R16G16B16A16Unorm, new FormatInfo(Format.R16G16B16A16Unorm, 1, 1, 8, 4) }, + { TextureFormat.R16G16B16A16Snorm, new FormatInfo(Format.R16G16B16A16Snorm, 1, 1, 8, 4) }, + { TextureFormat.R16G16B16A16Uint, new FormatInfo(Format.R16G16B16A16Uint, 1, 1, 8, 4) }, + { TextureFormat.R16G16B16A16Sint, new FormatInfo(Format.R16G16B16A16Sint, 1, 1, 8, 4) }, + { TextureFormat.R32G32B32A32Float, new FormatInfo(Format.R32G32B32A32Float, 1, 1, 16, 4) }, + { TextureFormat.R32G32B32A32Uint, new FormatInfo(Format.R32G32B32A32Uint, 1, 1, 16, 4) }, + { TextureFormat.R32G32B32A32Sint, new FormatInfo(Format.R32G32B32A32Sint, 1, 1, 16, 4) }, + { TextureFormat.Z16Unorm, new FormatInfo(Format.D16Unorm, 1, 1, 2, 1) }, + { TextureFormat.Zf32RFloatGUintBUintAUint, new FormatInfo(Format.D32Float, 1, 1, 4, 1) }, + { TextureFormat.Zf32Float, new FormatInfo(Format.D32Float, 1, 1, 4, 1) }, + { TextureFormat.G24R8RUintGUnormBUnormAUnorm, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) }, + { TextureFormat.Z24S8RUintGUnormBUnormAUnorm, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) }, + { TextureFormat.Z24S8RUintGUnormBUintAUint, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) }, + { TextureFormat.S8Z24RUnormGUintBUintAUint, new FormatInfo(Format.S8UintD24Unorm, 1, 1, 4, 2) }, + { TextureFormat.R32B24G8RFloatGUintBUnormAUnorm, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) }, + { TextureFormat.Zf32X24S8RFloatGUintBUnormAUnorm, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) }, + { TextureFormat.A8B8G8R8UnormSrgb, new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4) }, + { TextureFormat.G4R4Unorm, new FormatInfo(Format.R4G4Unorm, 1, 1, 1, 2) }, + { TextureFormat.A4B4G4R4Unorm, new FormatInfo(Format.R4G4B4A4Unorm, 1, 1, 2, 4) }, + { TextureFormat.A1B5G5R5Unorm, new FormatInfo(Format.R5G5B5A1Unorm, 1, 1, 2, 4) }, + { TextureFormat.B5G6R5Unorm, new FormatInfo(Format.R5G6B5Unorm, 1, 1, 2, 3) }, + { TextureFormat.A2B10G10R10Unorm, new FormatInfo(Format.R10G10B10A2Unorm, 1, 1, 4, 4) }, + { TextureFormat.A2B10G10R10Uint, new FormatInfo(Format.R10G10B10A2Uint, 1, 1, 4, 4) }, + { TextureFormat.Bf10Gf11Rf11Float, new FormatInfo(Format.R11G11B10Float, 1, 1, 4, 3) }, + { TextureFormat.E5B9G9R9SharedExpFloat, new FormatInfo(Format.R9G9B9E5Float, 1, 1, 4, 4) }, + { TextureFormat.Bc1Unorm, new FormatInfo(Format.Bc1RgbaUnorm, 4, 4, 8, 4) }, + { TextureFormat.Bc2Unorm, new FormatInfo(Format.Bc2Unorm, 4, 4, 16, 4) }, + { TextureFormat.Bc3Unorm, new FormatInfo(Format.Bc3Unorm, 4, 4, 16, 4) }, + { TextureFormat.Bc1UnormSrgb, new FormatInfo(Format.Bc1RgbaSrgb, 4, 4, 8, 4) }, + { TextureFormat.Bc2UnormSrgb, new FormatInfo(Format.Bc2Srgb, 4, 4, 16, 4) }, + { TextureFormat.Bc3UnormSrgb, new FormatInfo(Format.Bc3Srgb, 4, 4, 16, 4) }, + { TextureFormat.Bc4Unorm, new FormatInfo(Format.Bc4Unorm, 4, 4, 8, 1) }, + { TextureFormat.Bc4Snorm, new FormatInfo(Format.Bc4Snorm, 4, 4, 8, 1) }, + { TextureFormat.Bc5Unorm, new FormatInfo(Format.Bc5Unorm, 4, 4, 16, 2) }, + { TextureFormat.Bc5Snorm, new FormatInfo(Format.Bc5Snorm, 4, 4, 16, 2) }, + { TextureFormat.Bc7UUnorm, new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4) }, + { TextureFormat.Bc7UUnormSrgb, new FormatInfo(Format.Bc7Srgb, 4, 4, 16, 4) }, + { TextureFormat.Bc6HSf16Float, new FormatInfo(Format.Bc6HSfloat, 4, 4, 16, 4) }, + { TextureFormat.Bc6HUf16Float, new FormatInfo(Format.Bc6HUfloat, 4, 4, 16, 4) }, + { TextureFormat.Etc2RgbUnorm, new FormatInfo(Format.Etc2RgbUnorm, 4, 4, 8, 3) }, + { TextureFormat.Etc2RgbPtaUnorm, new FormatInfo(Format.Etc2RgbPtaUnorm, 4, 4, 8, 4) }, + { TextureFormat.Etc2RgbaUnorm, new FormatInfo(Format.Etc2RgbaUnorm, 4, 4, 16, 4) }, + { TextureFormat.Etc2RgbUnormSrgb, new FormatInfo(Format.Etc2RgbSrgb, 4, 4, 8, 3) }, + { TextureFormat.Etc2RgbPtaUnormSrgb, new FormatInfo(Format.Etc2RgbPtaSrgb, 4, 4, 8, 4) }, + { TextureFormat.Etc2RgbaUnormSrgb, new FormatInfo(Format.Etc2RgbaSrgb, 4, 4, 16, 4) }, + { TextureFormat.Astc2D4x4Unorm, new FormatInfo(Format.Astc4x4Unorm, 4, 4, 16, 4) }, + { TextureFormat.Astc2D5x4Unorm, new FormatInfo(Format.Astc5x4Unorm, 5, 4, 16, 4) }, + { TextureFormat.Astc2D5x5Unorm, new FormatInfo(Format.Astc5x5Unorm, 5, 5, 16, 4) }, + { TextureFormat.Astc2D6x5Unorm, new FormatInfo(Format.Astc6x5Unorm, 6, 5, 16, 4) }, + { TextureFormat.Astc2D6x6Unorm, new FormatInfo(Format.Astc6x6Unorm, 6, 6, 16, 4) }, + { TextureFormat.Astc2D8x5Unorm, new FormatInfo(Format.Astc8x5Unorm, 8, 5, 16, 4) }, + { TextureFormat.Astc2D8x6Unorm, new FormatInfo(Format.Astc8x6Unorm, 8, 6, 16, 4) }, + { TextureFormat.Astc2D8x8Unorm, new FormatInfo(Format.Astc8x8Unorm, 8, 8, 16, 4) }, + { TextureFormat.Astc2D10x5Unorm, new FormatInfo(Format.Astc10x5Unorm, 10, 5, 16, 4) }, + { TextureFormat.Astc2D10x6Unorm, new FormatInfo(Format.Astc10x6Unorm, 10, 6, 16, 4) }, + { TextureFormat.Astc2D10x8Unorm, new FormatInfo(Format.Astc10x8Unorm, 10, 8, 16, 4) }, + { TextureFormat.Astc2D10x10Unorm, new FormatInfo(Format.Astc10x10Unorm, 10, 10, 16, 4) }, + { TextureFormat.Astc2D12x10Unorm, new FormatInfo(Format.Astc12x10Unorm, 12, 10, 16, 4) }, + { TextureFormat.Astc2D12x12Unorm, new FormatInfo(Format.Astc12x12Unorm, 12, 12, 16, 4) }, + { TextureFormat.Astc2D4x4UnormSrgb, new FormatInfo(Format.Astc4x4Srgb, 4, 4, 16, 4) }, + { TextureFormat.Astc2D5x4UnormSrgb, new FormatInfo(Format.Astc5x4Srgb, 5, 4, 16, 4) }, + { TextureFormat.Astc2D5x5UnormSrgb, new FormatInfo(Format.Astc5x5Srgb, 5, 5, 16, 4) }, + { TextureFormat.Astc2D6x5UnormSrgb, new FormatInfo(Format.Astc6x5Srgb, 6, 5, 16, 4) }, + { TextureFormat.Astc2D6x6UnormSrgb, new FormatInfo(Format.Astc6x6Srgb, 6, 6, 16, 4) }, + { TextureFormat.Astc2D8x5UnormSrgb, new FormatInfo(Format.Astc8x5Srgb, 8, 5, 16, 4) }, + { TextureFormat.Astc2D8x6UnormSrgb, new FormatInfo(Format.Astc8x6Srgb, 8, 6, 16, 4) }, + { TextureFormat.Astc2D8x8UnormSrgb, new FormatInfo(Format.Astc8x8Srgb, 8, 8, 16, 4) }, + { TextureFormat.Astc2D10x5UnormSrgb, new FormatInfo(Format.Astc10x5Srgb, 10, 5, 16, 4) }, + { TextureFormat.Astc2D10x6UnormSrgb, new FormatInfo(Format.Astc10x6Srgb, 10, 6, 16, 4) }, + { TextureFormat.Astc2D10x8UnormSrgb, new FormatInfo(Format.Astc10x8Srgb, 10, 8, 16, 4) }, + { TextureFormat.Astc2D10x10UnormSrgb, new FormatInfo(Format.Astc10x10Srgb, 10, 10, 16, 4) }, + { TextureFormat.Astc2D12x10UnormSrgb, new FormatInfo(Format.Astc12x10Srgb, 12, 10, 16, 4) }, + { TextureFormat.Astc2D12x12UnormSrgb, new FormatInfo(Format.Astc12x12Srgb, 12, 12, 16, 4) }, + { TextureFormat.A5B5G5R1Unorm, new FormatInfo(Format.A1B5G5R5Unorm, 1, 1, 2, 4) } + }; + + private static readonly Dictionary<VertexAttributeFormat, Format> _attribFormats = new Dictionary<VertexAttributeFormat, Format>() + { + { VertexAttributeFormat.R8Unorm, Format.R8Unorm }, + { VertexAttributeFormat.R8Snorm, Format.R8Snorm }, + { VertexAttributeFormat.R8Uint, Format.R8Uint }, + { VertexAttributeFormat.R8Sint, Format.R8Sint }, + { VertexAttributeFormat.R16Float, Format.R16Float }, + { VertexAttributeFormat.R16Unorm, Format.R16Unorm }, + { VertexAttributeFormat.R16Snorm, Format.R16Snorm }, + { VertexAttributeFormat.R16Uint, Format.R16Uint }, + { VertexAttributeFormat.R16Sint, Format.R16Sint }, + { VertexAttributeFormat.R32Float, Format.R32Float }, + { VertexAttributeFormat.R32Uint, Format.R32Uint }, + { VertexAttributeFormat.R32Sint, Format.R32Sint }, + { VertexAttributeFormat.R8G8Unorm, Format.R8G8Unorm }, + { VertexAttributeFormat.R8G8Snorm, Format.R8G8Snorm }, + { VertexAttributeFormat.R8G8Uint, Format.R8G8Uint }, + { VertexAttributeFormat.R8G8Sint, Format.R8G8Sint }, + { VertexAttributeFormat.R16G16Float, Format.R16G16Float }, + { VertexAttributeFormat.R16G16Unorm, Format.R16G16Unorm }, + { VertexAttributeFormat.R16G16Snorm, Format.R16G16Snorm }, + { VertexAttributeFormat.R16G16Uint, Format.R16G16Uint }, + { VertexAttributeFormat.R16G16Sint, Format.R16G16Sint }, + { VertexAttributeFormat.R32G32Float, Format.R32G32Float }, + { VertexAttributeFormat.R32G32Uint, Format.R32G32Uint }, + { VertexAttributeFormat.R32G32Sint, Format.R32G32Sint }, + { VertexAttributeFormat.R8G8B8Unorm, Format.R8G8B8Unorm }, + { VertexAttributeFormat.R8G8B8Snorm, Format.R8G8B8Snorm }, + { VertexAttributeFormat.R8G8B8Uint, Format.R8G8B8Uint }, + { VertexAttributeFormat.R8G8B8Sint, Format.R8G8B8Sint }, + { VertexAttributeFormat.R16G16B16Float, Format.R16G16B16Float }, + { VertexAttributeFormat.R16G16B16Unorm, Format.R16G16B16Unorm }, + { VertexAttributeFormat.R16G16B16Snorm, Format.R16G16B16Snorm }, + { VertexAttributeFormat.R16G16B16Uint, Format.R16G16B16Uint }, + { VertexAttributeFormat.R16G16B16Sint, Format.R16G16B16Sint }, + { VertexAttributeFormat.R32G32B32Float, Format.R32G32B32Float }, + { VertexAttributeFormat.R32G32B32Uint, Format.R32G32B32Uint }, + { VertexAttributeFormat.R32G32B32Sint, Format.R32G32B32Sint }, + { VertexAttributeFormat.R8G8B8A8Unorm, Format.R8G8B8A8Unorm }, + { VertexAttributeFormat.R8G8B8A8Snorm, Format.R8G8B8A8Snorm }, + { VertexAttributeFormat.R8G8B8A8Uint, Format.R8G8B8A8Uint }, + { VertexAttributeFormat.R8G8B8A8Sint, Format.R8G8B8A8Sint }, + { VertexAttributeFormat.R16G16B16A16Float, Format.R16G16B16A16Float }, + { VertexAttributeFormat.R16G16B16A16Unorm, Format.R16G16B16A16Unorm }, + { VertexAttributeFormat.R16G16B16A16Snorm, Format.R16G16B16A16Snorm }, + { VertexAttributeFormat.R16G16B16A16Uint, Format.R16G16B16A16Uint }, + { VertexAttributeFormat.R16G16B16A16Sint, Format.R16G16B16A16Sint }, + { VertexAttributeFormat.R32G32B32A32Float, Format.R32G32B32A32Float }, + { VertexAttributeFormat.R32G32B32A32Uint, Format.R32G32B32A32Uint }, + { VertexAttributeFormat.R32G32B32A32Sint, Format.R32G32B32A32Sint }, + { VertexAttributeFormat.A2B10G10R10Unorm, Format.R10G10B10A2Unorm }, + { VertexAttributeFormat.A2B10G10R10Uint, Format.R10G10B10A2Uint }, + { VertexAttributeFormat.B10G11R11Float, Format.R11G11B10Float }, + { VertexAttributeFormat.R8Uscaled, Format.R8Uscaled }, + { VertexAttributeFormat.R8Sscaled, Format.R8Sscaled }, + { VertexAttributeFormat.R16Uscaled, Format.R16Uscaled }, + { VertexAttributeFormat.R16Sscaled, Format.R16Sscaled }, + { VertexAttributeFormat.R32Uscaled, Format.R32Uscaled }, + { VertexAttributeFormat.R32Sscaled, Format.R32Sscaled }, + { VertexAttributeFormat.R8G8Uscaled, Format.R8G8Uscaled }, + { VertexAttributeFormat.R8G8Sscaled, Format.R8G8Sscaled }, + { VertexAttributeFormat.R16G16Uscaled, Format.R16G16Uscaled }, + { VertexAttributeFormat.R16G16Sscaled, Format.R16G16Sscaled }, + { VertexAttributeFormat.R32G32Uscaled, Format.R32G32Uscaled }, + { VertexAttributeFormat.R32G32Sscaled, Format.R32G32Sscaled }, + { VertexAttributeFormat.R8G8B8Uscaled, Format.R8G8B8Uscaled }, + { VertexAttributeFormat.R8G8B8Sscaled, Format.R8G8B8Sscaled }, + { VertexAttributeFormat.R16G16B16Uscaled, Format.R16G16B16Uscaled }, + { VertexAttributeFormat.R16G16B16Sscaled, Format.R16G16B16Sscaled }, + { VertexAttributeFormat.R32G32B32Uscaled, Format.R32G32B32Uscaled }, + { VertexAttributeFormat.R32G32B32Sscaled, Format.R32G32B32Sscaled }, + { VertexAttributeFormat.R8G8B8A8Uscaled, Format.R8G8B8A8Uscaled }, + { VertexAttributeFormat.R8G8B8A8Sscaled, Format.R8G8B8A8Sscaled }, + { VertexAttributeFormat.R16G16B16A16Uscaled, Format.R16G16B16A16Uscaled }, + { VertexAttributeFormat.R16G16B16A16Sscaled, Format.R16G16B16A16Sscaled }, + { VertexAttributeFormat.R32G32B32A32Uscaled, Format.R32G32B32A32Uscaled }, + { VertexAttributeFormat.R32G32B32A32Sscaled, Format.R32G32B32A32Sscaled }, + { VertexAttributeFormat.A2B10G10R10Snorm, Format.R10G10B10A2Snorm }, + { VertexAttributeFormat.A2B10G10R10Sint, Format.R10G10B10A2Sint }, + { VertexAttributeFormat.A2B10G10R10Uscaled, Format.R10G10B10A2Uscaled }, + { VertexAttributeFormat.A2B10G10R10Sscaled, Format.R10G10B10A2Sscaled } + }; + + /// <summary> + /// Try getting the texture format from an encoded format integer from the Maxwell texture descriptor. + /// </summary> + /// <param name="encoded">The encoded format integer from the texture descriptor</param> + /// <param name="isSrgb">Indicates if the format is a sRGB format</param> + /// <param name="format">The output texture format</param> + /// <returns>True if the format is valid, false otherwise</returns> + public static bool TryGetTextureFormat(uint encoded, bool isSrgb, out FormatInfo format) + { + encoded |= (isSrgb ? 1u << 19 : 0u); + + return _textureFormats.TryGetValue((TextureFormat)encoded, out format); + } + + /// <summary> + /// Try getting the vertex attribute format from an encoded format integer from Maxwell attribute registers. + /// </summary> + /// <param name="encoded">The encoded format integer from the attribute registers</param> + /// <param name="format">The output vertex attribute format</param> + /// <returns>True if the format is valid, false otherwise</returns> + public static bool TryGetAttribFormat(uint encoded, out Format format) + { + return _attribFormats.TryGetValue((VertexAttributeFormat)encoded, out format); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/ITextureDescriptor.cs b/src/Ryujinx.Graphics.Gpu/Image/ITextureDescriptor.cs new file mode 100644 index 00000000..378de44b --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/ITextureDescriptor.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + interface ITextureDescriptor + { + public uint UnpackFormat(); + public TextureTarget UnpackTextureTarget(); + public bool UnpackSrgb(); + public bool UnpackTextureCoordNormalized(); + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/Pool.cs b/src/Ryujinx.Graphics.Gpu/Image/Pool.cs new file mode 100644 index 00000000..3e557c0b --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/Pool.cs @@ -0,0 +1,222 @@ +using Ryujinx.Cpu.Tracking; +using Ryujinx.Graphics.Gpu.Memory; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Represents a pool of GPU resources, such as samplers or textures. + /// </summary> + /// <typeparam name="T1">Type of the GPU resource</typeparam> + /// <typeparam name="T2">Type of the descriptor</typeparam> + abstract class Pool<T1, T2> : IDisposable where T2 : unmanaged + { + protected const int DescriptorSize = 0x20; + + protected GpuContext Context; + protected PhysicalMemory PhysicalMemory; + protected int SequenceNumber; + protected int ModifiedSequenceNumber; + + protected T1[] Items; + protected T2[] DescriptorCache; + + /// <summary> + /// The maximum ID value of resources on the pool (inclusive). + /// </summary> + /// <remarks> + /// The maximum amount of resources on the pool is equal to this value plus one. + /// </remarks> + public int MaximumId { get; } + + /// <summary> + /// The address of the pool in guest memory. + /// </summary> + public ulong Address { get; } + + /// <summary> + /// The size of the pool in bytes. + /// </summary> + public ulong Size { get; } + + private readonly CpuMultiRegionHandle _memoryTracking; + private readonly Action<ulong, ulong> _modifiedDelegate; + + private int _modifiedSequenceOffset; + private bool _modified; + + /// <summary> + /// Creates a new instance of the GPU resource pool. + /// </summary> + /// <param name="context">GPU context that the pool belongs to</param> + /// <param name="physicalMemory">Physical memory where the resource descriptors are mapped</param> + /// <param name="address">Address of the pool in physical memory</param> + /// <param name="maximumId">Maximum index of an item on the pool (inclusive)</param> + public Pool(GpuContext context, PhysicalMemory physicalMemory, ulong address, int maximumId) + { + Context = context; + PhysicalMemory = physicalMemory; + MaximumId = maximumId; + + int count = maximumId + 1; + + ulong size = (ulong)(uint)count * DescriptorSize; + + Items = new T1[count]; + DescriptorCache = new T2[count]; + + Address = address; + Size = size; + + _memoryTracking = physicalMemory.BeginGranularTracking(address, size, ResourceKind.Pool); + _memoryTracking.RegisterPreciseAction(address, size, PreciseAction); + _modifiedDelegate = RegionModified; + } + + /// <summary> + /// Gets the descriptor for a given ID. + /// </summary> + /// <param name="id">ID of the descriptor. This is effectively a zero-based index</param> + /// <returns>The descriptor</returns> + public T2 GetDescriptor(int id) + { + return PhysicalMemory.Read<T2>(Address + (ulong)id * DescriptorSize); + } + + /// <summary> + /// Gets a reference to the descriptor for a given ID. + /// </summary> + /// <param name="id">ID of the descriptor. This is effectively a zero-based index</param> + /// <returns>A reference to the descriptor</returns> + public ref readonly T2 GetDescriptorRef(int id) + { + return ref GetDescriptorRefAddress(Address + (ulong)id * DescriptorSize); + } + + /// <summary> + /// Gets a reference to the descriptor for a given address. + /// </summary> + /// <param name="address">Address of the descriptor</param> + /// <returns>A reference to the descriptor</returns> + public ref readonly T2 GetDescriptorRefAddress(ulong address) + { + return ref MemoryMarshal.Cast<byte, T2>(PhysicalMemory.GetSpan(address, DescriptorSize))[0]; + } + + /// <summary> + /// Gets the GPU resource with the given ID. + /// </summary> + /// <param name="id">ID of the resource. This is effectively a zero-based index</param> + /// <returns>The GPU resource with the given ID</returns> + public abstract T1 Get(int id); + + /// <summary> + /// Checks if a given ID is valid and inside the range of the pool. + /// </summary> + /// <param name="id">ID of the descriptor. This is effectively a zero-based index</param> + /// <returns>True if the specified ID is valid, false otherwise</returns> + public bool IsValidId(int id) + { + return (uint)id <= MaximumId; + } + + /// <summary> + /// Synchronizes host memory with guest memory. + /// This causes invalidation of pool entries, + /// if a modification of entries by the CPU is detected. + /// </summary> + public void SynchronizeMemory() + { + _modified = false; + _memoryTracking.QueryModified(_modifiedDelegate); + + if (_modified) + { + UpdateModifiedSequence(); + } + } + + /// <summary> + /// Indicate that a region of the pool was modified, and must be loaded from memory. + /// </summary> + /// <param name="mAddress">Start address of the modified region</param> + /// <param name="mSize">Size of the modified region</param> + private void RegionModified(ulong mAddress, ulong mSize) + { + _modified = true; + + if (mAddress < Address) + { + mAddress = Address; + } + + ulong maxSize = Address + Size - mAddress; + + if (mSize > maxSize) + { + mSize = maxSize; + } + + InvalidateRangeImpl(mAddress, mSize); + } + + /// <summary> + /// Updates the modified sequence number using the current sequence number and offset, + /// indicating that it has been modified. + /// </summary> + protected void UpdateModifiedSequence() + { + ModifiedSequenceNumber = SequenceNumber + _modifiedSequenceOffset; + } + + /// <summary> + /// An action to be performed when a precise memory access occurs to this resource. + /// Makes sure that the dirty flags are checked. + /// </summary> + /// <param name="address">Address of the memory action</param> + /// <param name="size">Size in bytes</param> + /// <param name="write">True if the access was a write, false otherwise</param> + private bool PreciseAction(ulong address, ulong size, bool write) + { + if (write && Context.SequenceNumber == SequenceNumber) + { + if (ModifiedSequenceNumber == SequenceNumber + _modifiedSequenceOffset) + { + // The modified sequence number is offset when PreciseActions occur so that + // users checking it will see an increment and know the pool has changed since + // their last look, even though the main SequenceNumber has not been changed. + + _modifiedSequenceOffset++; + } + + // Force the pool to be checked again the next time it is used. + SequenceNumber--; + } + + return false; + } + + protected abstract void InvalidateRangeImpl(ulong address, ulong size); + + protected abstract void Delete(T1 item); + + /// <summary> + /// Performs the disposal of all resources stored on the pool. + /// It's an error to try using the pool after disposal. + /// </summary> + public virtual void Dispose() + { + if (Items != null) + { + for (int index = 0; index < Items.Length; index++) + { + Delete(Items[index]); + } + + Items = null; + } + _memoryTracking.Dispose(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs b/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs new file mode 100644 index 00000000..e1493f38 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Resource pool interface. + /// </summary> + /// <typeparam name="T">Resource pool type</typeparam> + interface IPool<T> + { + /// <summary> + /// Start address of the pool in memory. + /// </summary> + ulong Address { get; } + + /// <summary> + /// Linked list node used on the texture pool cache. + /// </summary> + LinkedListNode<T> CacheNode { get; set; } + + /// <summary> + /// Timestamp set on the last use of the pool by the cache. + /// </summary> + ulong CacheTimestamp { get; set; } + } + + /// <summary> + /// Pool cache. + /// This can keep multiple pools, and return the current one as needed. + /// </summary> + abstract class PoolCache<T> : IDisposable where T : IPool<T>, IDisposable + { + private const int MaxCapacity = 2; + private const ulong MinDeltaForRemoval = 20000; + + private readonly GpuContext _context; + private readonly LinkedList<T> _pools; + private ulong _currentTimestamp; + + /// <summary> + /// Constructs a new instance of the pool. + /// </summary> + /// <param name="context">GPU context that the texture pool belongs to</param> + public PoolCache(GpuContext context) + { + _context = context; + _pools = new LinkedList<T>(); + } + + /// <summary> + /// Increments the internal timestamp of the cache that is used to decide when old resources will be deleted. + /// </summary> + public void Tick() + { + _currentTimestamp++; + } + + /// <summary> + /// Finds a cache texture pool, or creates a new one if not found. + /// </summary> + /// <param name="channel">GPU channel that the texture pool cache belongs to</param> + /// <param name="address">Start address of the texture pool</param> + /// <param name="maximumId">Maximum ID of the texture pool</param> + /// <returns>The found or newly created texture pool</returns> + public T FindOrCreate(GpuChannel channel, ulong address, int maximumId) + { + // Remove old entries from the cache, if possible. + while (_pools.Count > MaxCapacity && (_currentTimestamp - _pools.First.Value.CacheTimestamp) >= MinDeltaForRemoval) + { + T oldestPool = _pools.First.Value; + + _pools.RemoveFirst(); + oldestPool.Dispose(); + oldestPool.CacheNode = null; + } + + T pool; + + // Try to find the pool on the cache. + for (LinkedListNode<T> node = _pools.First; node != null; node = node.Next) + { + pool = node.Value; + + if (pool.Address == address) + { + if (pool.CacheNode != _pools.Last) + { + _pools.Remove(pool.CacheNode); + + pool.CacheNode = _pools.AddLast(pool); + } + + pool.CacheTimestamp = _currentTimestamp; + + return pool; + } + } + + // If not found, create a new one. + pool = CreatePool(_context, channel, address, maximumId); + + pool.CacheNode = _pools.AddLast(pool); + pool.CacheTimestamp = _currentTimestamp; + + return pool; + } + + /// <summary> + /// Creates a new instance of the pool. + /// </summary> + /// <param name="context">GPU context that the pool belongs to</param> + /// <param name="channel">GPU channel that the pool belongs to</param> + /// <param name="address">Address of the pool in guest memory</param> + /// <param name="maximumId">Maximum ID of the pool (equal to maximum minus one)</param> + protected abstract T CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId); + + public void Dispose() + { + foreach (T pool in _pools) + { + pool.Dispose(); + pool.CacheNode = null; + } + + _pools.Clear(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/ReductionFilter.cs b/src/Ryujinx.Graphics.Gpu/Image/ReductionFilter.cs new file mode 100644 index 00000000..1f7d9b07 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/ReductionFilter.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Represents a filter used with texture minification linear filtering. + /// </summary> + /// <remarks> + /// This feature is only supported on NVIDIA GPUs. + /// </remarks> + enum ReductionFilter + { + Average, + Minimum, + Maximum + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs b/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs new file mode 100644 index 00000000..b70ac9eb --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/Sampler.cs @@ -0,0 +1,115 @@ +using Ryujinx.Graphics.GAL; +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Cached sampler entry for sampler pools. + /// </summary> + class Sampler : IDisposable + { + /// <summary> + /// True if the sampler is disposed, false otherwise. + /// </summary> + public bool IsDisposed { get; private set; } + + /// <summary> + /// Host sampler object. + /// </summary> + private readonly ISampler _hostSampler; + + /// <summary> + /// Host sampler object, with anisotropy forced. + /// </summary> + private readonly ISampler _anisoSampler; + + /// <summary> + /// Creates a new instance of the cached sampler. + /// </summary> + /// <param name="context">The GPU context the sampler belongs to</param> + /// <param name="descriptor">The Maxwell sampler descriptor</param> + public Sampler(GpuContext context, SamplerDescriptor descriptor) + { + MinFilter minFilter = descriptor.UnpackMinFilter(); + MagFilter magFilter = descriptor.UnpackMagFilter(); + + bool seamlessCubemap = descriptor.UnpackSeamlessCubemap(); + + AddressMode addressU = descriptor.UnpackAddressU(); + AddressMode addressV = descriptor.UnpackAddressV(); + AddressMode addressP = descriptor.UnpackAddressP(); + + CompareMode compareMode = descriptor.UnpackCompareMode(); + CompareOp compareOp = descriptor.UnpackCompareOp(); + + ColorF color = new ColorF( + descriptor.BorderColorR, + descriptor.BorderColorG, + descriptor.BorderColorB, + descriptor.BorderColorA); + + float minLod = descriptor.UnpackMinLod(); + float maxLod = descriptor.UnpackMaxLod(); + float mipLodBias = descriptor.UnpackMipLodBias(); + + float maxRequestedAnisotropy = descriptor.UnpackMaxAnisotropy(); + float maxSupportedAnisotropy = context.Capabilities.MaximumSupportedAnisotropy; + + _hostSampler = context.Renderer.CreateSampler(new SamplerCreateInfo( + minFilter, + magFilter, + seamlessCubemap, + addressU, + addressV, + addressP, + compareMode, + compareOp, + color, + minLod, + maxLod, + mipLodBias, + Math.Min(maxRequestedAnisotropy, maxSupportedAnisotropy))); + + if (GraphicsConfig.MaxAnisotropy >= 0 && GraphicsConfig.MaxAnisotropy <= 16 && (minFilter == MinFilter.LinearMipmapNearest || minFilter == MinFilter.LinearMipmapLinear)) + { + maxRequestedAnisotropy = GraphicsConfig.MaxAnisotropy; + + _anisoSampler = context.Renderer.CreateSampler(new SamplerCreateInfo( + minFilter, + magFilter, + seamlessCubemap, + addressU, + addressV, + addressP, + compareMode, + compareOp, + color, + minLod, + maxLod, + mipLodBias, + Math.Min(maxRequestedAnisotropy, maxSupportedAnisotropy))); + } + } + + /// <summary> + /// Gets a host sampler for the given texture. + /// </summary> + /// <param name="texture">Texture to be sampled</param> + /// <returns>A host sampler</returns> + public ISampler GetHostSampler(Texture texture) + { + return _anisoSampler != null && texture?.CanForceAnisotropy == true ? _anisoSampler : _hostSampler; + } + + /// <summary> + /// Disposes the host sampler object. + /// </summary> + public void Dispose() + { + IsDisposed = true; + + _hostSampler.Dispose(); + _anisoSampler?.Dispose(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs new file mode 100644 index 00000000..64a146fb --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs @@ -0,0 +1,260 @@ +using Ryujinx.Graphics.GAL; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Maxwell sampler descriptor structure. + /// This structure defines the sampler descriptor as it is packed on the GPU sampler pool region. + /// </summary> + struct SamplerDescriptor + { + private static readonly float[] _f5ToF32ConversionLut = new float[] + { + 0.0f, + 0.055555556f, + 0.1f, + 0.13636364f, + 0.16666667f, + 0.1923077f, + 0.21428572f, + 0.23333333f, + 0.25f, + 0.2777778f, + 0.3f, + 0.3181818f, + 0.33333334f, + 0.34615386f, + 0.35714287f, + 0.36666667f, + 0.375f, + 0.3888889f, + 0.4f, + 0.4090909f, + 0.41666666f, + 0.42307693f, + 0.42857143f, + 0.43333334f, + 0.4375f, + 0.44444445f, + 0.45f, + 0.45454547f, + 0.45833334f, + 0.46153846f, + 0.4642857f, + 0.46666667f + }; + + private static readonly float[] _maxAnisotropyLut = new float[] + { + 1, 2, 4, 6, 8, 10, 12, 16 + }; + + private const float Frac8ToF32 = 1.0f / 256.0f; + +#pragma warning disable CS0649 + public uint Word0; + public uint Word1; + public uint Word2; + public uint Word3; + public float BorderColorR; + public float BorderColorG; + public float BorderColorB; + public float BorderColorA; +#pragma warning restore CS0649 + + /// <summary> + /// Unpacks the texture wrap mode along the X axis. + /// </summary> + /// <returns>The texture wrap mode enum</returns> + public AddressMode UnpackAddressU() + { + return (AddressMode)(Word0 & 7); + } + + // <summary> + /// Unpacks the texture wrap mode along the Y axis. + /// </summary> + /// <returns>The texture wrap mode enum</returns> + public AddressMode UnpackAddressV() + { + return (AddressMode)((Word0 >> 3) & 7); + } + + // <summary> + /// Unpacks the texture wrap mode along the Z axis. + /// </summary> + /// <returns>The texture wrap mode enum</returns> + public AddressMode UnpackAddressP() + { + return (AddressMode)((Word0 >> 6) & 7); + } + + /// <summary> + /// Unpacks the compare mode used for depth comparison on the shader, for + /// depth buffer texture. + /// This is only relevant for shaders with shadow samplers. + /// </summary> + /// <returns>The depth comparison mode enum</returns> + public CompareMode UnpackCompareMode() + { + return (CompareMode)((Word0 >> 9) & 1); + } + + /// <summary> + /// Unpacks the compare operation used for depth comparison on the shader, for + /// depth buffer texture. + /// This is only relevant for shaders with shadow samplers. + /// </summary> + /// <returns>The depth comparison operation enum</returns> + public CompareOp UnpackCompareOp() + { + return (CompareOp)(((Word0 >> 10) & 7) + 1); + } + + /// <summary> + /// Unpacks and converts the maximum anisotropy value used for texture anisotropic filtering. + /// </summary> + /// <returns>The maximum anisotropy</returns> + public float UnpackMaxAnisotropy() + { + return _maxAnisotropyLut[(Word0 >> 20) & 7]; + } + + /// <summary> + /// Unpacks the texture magnification filter. + /// This defines the filtering used when the texture covers an area on the screen + /// that is larger than the texture size. + /// </summary> + /// <returns>The magnification filter</returns> + public MagFilter UnpackMagFilter() + { + return (MagFilter)(Word1 & 3); + } + + /// <summary> + /// Unpacks the texture minification filter. + /// This defines the filtering used when the texture covers an area on the screen + /// that is smaller than the texture size. + /// </summary> + /// <returns>The minification filter</returns> + public MinFilter UnpackMinFilter() + { + SamplerMinFilter minFilter = (SamplerMinFilter)((Word1 >> 4) & 3); + SamplerMipFilter mipFilter = (SamplerMipFilter)((Word1 >> 6) & 3); + + return ConvertFilter(minFilter, mipFilter); + } + + /// <summary> + /// Converts two minification and filter enum, to a single minification enum, + /// including mipmap filtering information, as expected from the host API. + /// </summary> + /// <param name="minFilter">The minification filter</param> + /// <param name="mipFilter">The mipmap level filter</param> + /// <returns>The combined, host API compatible filter enum</returns> + private static MinFilter ConvertFilter(SamplerMinFilter minFilter, SamplerMipFilter mipFilter) + { + switch (mipFilter) + { + case SamplerMipFilter.None: + switch (minFilter) + { + case SamplerMinFilter.Nearest: return MinFilter.Nearest; + case SamplerMinFilter.Linear: return MinFilter.Linear; + } + break; + + case SamplerMipFilter.Nearest: + switch (minFilter) + { + case SamplerMinFilter.Nearest: return MinFilter.NearestMipmapNearest; + case SamplerMinFilter.Linear: return MinFilter.LinearMipmapNearest; + } + break; + + case SamplerMipFilter.Linear: + switch (minFilter) + { + case SamplerMinFilter.Nearest: return MinFilter.NearestMipmapLinear; + case SamplerMinFilter.Linear: return MinFilter.LinearMipmapLinear; + } + break; + } + + return MinFilter.Nearest; + } + + /// <summary> + /// Unpacks the seamless cubemap flag. + /// </summary> + /// <returns>The seamless cubemap flag</returns> + public bool UnpackSeamlessCubemap() + { + return (Word1 & (1 << 9)) != 0; + } + + /// <summary> + /// Unpacks the reduction filter, used with texture minification linear filtering. + /// This describes how the final value will be computed from neighbouring pixels. + /// </summary> + /// <returns>The reduction filter</returns> + public ReductionFilter UnpackReductionFilter() + { + return (ReductionFilter)((Word1 >> 10) & 3); + } + + /// <summary> + /// Unpacks the level-of-detail bias value. + /// This is a bias added to the level-of-detail value as computed by the GPU, used to select + /// which mipmap level to use from a given texture. + /// </summary> + /// <returns>The level-of-detail bias value</returns> + public float UnpackMipLodBias() + { + int fixedValue = (int)(Word1 >> 12) & 0x1fff; + + fixedValue = (fixedValue << 19) >> 19; + + return fixedValue * Frac8ToF32; + } + + /// <summary> + /// Unpacks the level-of-detail snap value. + /// </summary> + /// <returns>The level-of-detail snap value</returns> + public float UnpackLodSnap() + { + return _f5ToF32ConversionLut[(Word1 >> 26) & 0x1f]; + } + + /// <summary> + /// Unpacks the minimum level-of-detail value. + /// </summary> + /// <returns>The minimum level-of-detail value</returns> + public float UnpackMinLod() + { + return (Word2 & 0xfff) * Frac8ToF32; + } + + /// <summary> + /// Unpacks the maximum level-of-detail value. + /// </summary> + /// <returns>The maximum level-of-detail value</returns> + public float UnpackMaxLod() + { + return ((Word2 >> 12) & 0xfff) * Frac8ToF32; + } + + /// <summary> + /// Check if two descriptors are equal. + /// </summary> + /// <param name="other">The descriptor to compare against</param> + /// <returns>True if they are equal, false otherwise</returns> + public bool Equals(ref SamplerDescriptor other) + { + return Unsafe.As<SamplerDescriptor, Vector256<byte>>(ref this).Equals(Unsafe.As<SamplerDescriptor, Vector256<byte>>(ref other)); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerMinFilter.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerMinFilter.cs new file mode 100644 index 00000000..17beb129 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerMinFilter.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Sampler texture minification filter. + /// </summary> + enum SamplerMinFilter + { + Nearest = 1, + Linear + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerMipFilter.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerMipFilter.cs new file mode 100644 index 00000000..319d4196 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerMipFilter.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Sampler texture mipmap level filter. + /// </summary> + enum SamplerMipFilter + { + None = 1, + Nearest, + Linear + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs new file mode 100644 index 00000000..eb7222f9 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs @@ -0,0 +1,162 @@ +using Ryujinx.Graphics.Gpu.Memory; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Sampler pool. + /// </summary> + class SamplerPool : Pool<Sampler, SamplerDescriptor>, IPool<SamplerPool> + { + private float _forcedAnisotropy; + + /// <summary> + /// Linked list node used on the sampler pool cache. + /// </summary> + public LinkedListNode<SamplerPool> CacheNode { get; set; } + + /// <summary> + /// Timestamp used by the sampler pool cache, updated on every use of this sampler pool. + /// </summary> + public ulong CacheTimestamp { get; set; } + + /// <summary> + /// Creates a new instance of the sampler pool. + /// </summary> + /// <param name="context">GPU context that the sampler pool belongs to</param> + /// <param name="physicalMemory">Physical memory where the sampler descriptors are mapped</param> + /// <param name="address">Address of the sampler pool in guest memory</param> + /// <param name="maximumId">Maximum sampler ID of the sampler pool (equal to maximum samplers minus one)</param> + public SamplerPool(GpuContext context, PhysicalMemory physicalMemory, ulong address, int maximumId) : base(context, physicalMemory, address, maximumId) + { + _forcedAnisotropy = GraphicsConfig.MaxAnisotropy; + } + + /// <summary> + /// Gets the sampler with the given ID. + /// </summary> + /// <param name="id">ID of the sampler. This is effectively a zero-based index</param> + /// <returns>The sampler with the given ID</returns> + public override Sampler Get(int id) + { + if ((uint)id >= Items.Length) + { + return null; + } + + if (SequenceNumber != Context.SequenceNumber) + { + if (_forcedAnisotropy != GraphicsConfig.MaxAnisotropy) + { + _forcedAnisotropy = GraphicsConfig.MaxAnisotropy; + + for (int i = 0; i < Items.Length; i++) + { + if (Items[i] != null) + { + Items[i].Dispose(); + + Items[i] = null; + } + } + + UpdateModifiedSequence(); + } + + SequenceNumber = Context.SequenceNumber; + + SynchronizeMemory(); + } + + Sampler sampler = Items[id]; + + if (sampler == null) + { + SamplerDescriptor descriptor = GetDescriptor(id); + + sampler = new Sampler(Context, descriptor); + + Items[id] = sampler; + + DescriptorCache[id] = descriptor; + } + + return sampler; + } + + /// <summary> + /// Checks if the pool was modified, and returns the last sequence number where a modification was detected. + /// </summary> + /// <returns>A number that increments each time a modification is detected</returns> + public int CheckModified() + { + if (SequenceNumber != Context.SequenceNumber) + { + SequenceNumber = Context.SequenceNumber; + + if (_forcedAnisotropy != GraphicsConfig.MaxAnisotropy) + { + _forcedAnisotropy = GraphicsConfig.MaxAnisotropy; + + for (int i = 0; i < Items.Length; i++) + { + if (Items[i] != null) + { + Items[i].Dispose(); + + Items[i] = null; + } + } + + UpdateModifiedSequence(); + } + + SynchronizeMemory(); + } + + return ModifiedSequenceNumber; + } + + /// <summary> + /// Implementation of the sampler pool range invalidation. + /// </summary> + /// <param name="address">Start address of the range of the sampler pool</param> + /// <param name="size">Size of the range being invalidated</param> + protected override void InvalidateRangeImpl(ulong address, ulong size) + { + ulong endAddress = address + size; + + for (; address < endAddress; address += DescriptorSize) + { + int id = (int)((address - Address) / DescriptorSize); + + Sampler sampler = Items[id]; + + if (sampler != null) + { + SamplerDescriptor descriptor = GetDescriptor(id); + + // If the descriptors are the same, the sampler is still valid. + if (descriptor.Equals(ref DescriptorCache[id])) + { + continue; + } + + sampler.Dispose(); + + Items[id] = null; + } + } + } + + /// <summary> + /// Deletes a given sampler pool entry. + /// The host memory used by the sampler is released by the driver. + /// </summary> + /// <param name="item">The entry to be deleted</param> + protected override void Delete(Sampler item) + { + item?.Dispose(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs b/src/Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs new file mode 100644 index 00000000..3b3350fb --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs @@ -0,0 +1,30 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Sampler pool cache. + /// This can keep multiple sampler pools, and return the current one as needed. + /// It is useful for applications that uses multiple sampler pools. + /// </summary> + class SamplerPoolCache : PoolCache<SamplerPool> + { + /// <summary> + /// Constructs a new instance of the texture pool. + /// </summary> + /// <param name="context">GPU context that the texture pool belongs to</param> + public SamplerPoolCache(GpuContext context) : base(context) + { + } + + /// <summary> + /// Creates a new instance of the sampler pool. + /// </summary> + /// <param name="context">GPU context that the sampler pool belongs to</param> + /// <param name="channel">GPU channel that the texture pool belongs to</param> + /// <param name="address">Address of the sampler pool in guest memory</param> + /// <param name="maximumId">Maximum sampler ID of the sampler pool (equal to maximum samplers minus one)</param> + protected override SamplerPool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId) + { + return new SamplerPool(context, channel.MemoryManager.Physical, address, maximumId); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs new file mode 100644 index 00000000..84808a84 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -0,0 +1,1705 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Texture; +using Ryujinx.Graphics.Texture.Astc; +using Ryujinx.Memory; +using Ryujinx.Memory.Range; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Represents a cached GPU texture. + /// </summary> + class Texture : IMultiRangeItem, IDisposable + { + // How many updates we need before switching to the byte-by-byte comparison + // modification check method. + // This method uses much more memory so we want to avoid it if possible. + private const int ByteComparisonSwitchThreshold = 4; + + // Tuning for blacklisting textures from scaling when their data is updated from CPU. + // Each write adds the weight, each GPU modification subtracts 1. + // Exceeding the threshold blacklists the texture. + private const int ScaledSetWeight = 10; + private const int ScaledSetThreshold = 30; + + private const int MinLevelsForForceAnisotropy = 5; + + private struct TexturePoolOwner + { + public TexturePool Pool; + public int ID; + public ulong GpuAddress; + } + + private GpuContext _context; + private PhysicalMemory _physicalMemory; + + private SizeInfo _sizeInfo; + + /// <summary> + /// Texture format. + /// </summary> + public Format Format => Info.FormatInfo.Format; + + /// <summary> + /// Texture target. + /// </summary> + public Target Target { get; private set; } + + /// <summary> + /// Texture width. + /// </summary> + public int Width { get; private set; } + + /// <summary> + /// Texture height. + /// </summary> + public int Height { get; private set; } + + /// <summary> + /// Texture information. + /// </summary> + public TextureInfo Info { get; private set; } + + /// <summary> + /// Set when anisotropic filtering can be forced on the given texture. + /// </summary> + public bool CanForceAnisotropy { get; private set; } + + /// <summary> + /// Host scale factor. + /// </summary> + public float ScaleFactor { get; private set; } + + /// <summary> + /// Upscaling mode. Informs if a texture is scaled, or is eligible for scaling. + /// </summary> + public TextureScaleMode ScaleMode { get; private set; } + + /// <summary> + /// Group that this texture belongs to. Manages read/write memory tracking. + /// </summary> + public TextureGroup Group { get; private set; } + + /// <summary> + /// Set when a texture's GPU VA has ever been partially or fully unmapped. + /// This indicates that the range must be fully checked when matching the texture. + /// </summary> + public bool ChangedMapping { get; private set; } + + /// <summary> + /// True if the data for this texture must always be flushed when an overlap appears. + /// This is useful if SetData is called directly on this texture, but the data is meant for a future texture. + /// </summary> + public bool AlwaysFlushOnOverlap { get; private set; } + + /// <summary> + /// Increments when the host texture is swapped, or when the texture is removed from all pools. + /// </summary> + public int InvalidatedSequence { get; private set; } + + private int _depth; + private int _layers; + public int FirstLayer { get; private set; } + public int FirstLevel { get; private set; } + + private bool _hasData; + private bool _dirty = true; + private int _updateCount; + private byte[] _currentData; + + private bool _modifiedStale = true; + + private ITexture _arrayViewTexture; + private Target _arrayViewTarget; + + private ITexture _flushHostTexture; + private ITexture _setHostTexture; + private int _scaledSetScore; + + private Texture _viewStorage; + + private List<Texture> _views; + + /// <summary> + /// Host texture. + /// </summary> + public ITexture HostTexture { get; private set; } + + /// <summary> + /// Intrusive linked list node used on the auto deletion texture cache. + /// </summary> + public LinkedListNode<Texture> CacheNode { get; set; } + + /// <summary> + /// Entry for this texture in the short duration cache, if present. + /// </summary> + public ShortTextureCacheEntry ShortCacheEntry { get; set; } + + /// Physical memory ranges where the texture data is located. + /// </summary> + public MultiRange Range { get; private set; } + + /// <summary> + /// Layer size in bytes. + /// </summary> + public int LayerSize => _sizeInfo.LayerSize; + + /// <summary> + /// Texture size in bytes. + /// </summary> + public ulong Size => (ulong)_sizeInfo.TotalSize; + + /// <summary> + /// Whether or not the texture belongs is a view. + /// </summary> + public bool IsView => _viewStorage != this; + + /// <summary> + /// Whether or not this texture has views. + /// </summary> + public bool HasViews => _views.Count > 0; + + private int _referenceCount; + private List<TexturePoolOwner> _poolOwners; + + /// <summary> + /// Constructs a new instance of the cached GPU texture. + /// </summary> + /// <param name="context">GPU context that the texture belongs to</param> + /// <param name="physicalMemory">Physical memory where the texture is mapped</param> + /// <param name="info">Texture information</param> + /// <param name="sizeInfo">Size information of the texture</param> + /// <param name="range">Physical memory ranges where the texture data is located</param> + /// <param name="firstLayer">The first layer of the texture, or 0 if the texture has no parent</param> + /// <param name="firstLevel">The first mipmap level of the texture, or 0 if the texture has no parent</param> + /// <param name="scaleFactor">The floating point scale factor to initialize with</param> + /// <param name="scaleMode">The scale mode to initialize with</param> + private Texture( + GpuContext context, + PhysicalMemory physicalMemory, + TextureInfo info, + SizeInfo sizeInfo, + MultiRange range, + int firstLayer, + int firstLevel, + float scaleFactor, + TextureScaleMode scaleMode) + { + InitializeTexture(context, physicalMemory, info, sizeInfo, range); + + FirstLayer = firstLayer; + FirstLevel = firstLevel; + + ScaleFactor = scaleFactor; + ScaleMode = scaleMode; + + InitializeData(true); + } + + /// <summary> + /// Constructs a new instance of the cached GPU texture. + /// </summary> + /// <param name="context">GPU context that the texture belongs to</param> + /// <param name="physicalMemory">Physical memory where the texture is mapped</param> + /// <param name="info">Texture information</param> + /// <param name="sizeInfo">Size information of the texture</param> + /// <param name="range">Physical memory ranges where the texture data is located</param> + /// <param name="scaleMode">The scale mode to initialize with. If scaled, the texture's data is loaded immediately and scaled up</param> + public Texture( + GpuContext context, + PhysicalMemory physicalMemory, + TextureInfo info, + SizeInfo sizeInfo, + MultiRange range, + TextureScaleMode scaleMode) + { + ScaleFactor = 1f; // Texture is first loaded at scale 1x. + ScaleMode = scaleMode; + + InitializeTexture(context, physicalMemory, info, sizeInfo, range); + } + + /// <summary> + /// Common texture initialization method. + /// This sets the context, info and sizeInfo fields. + /// Other fields are initialized with their default values. + /// </summary> + /// <param name="context">GPU context that the texture belongs to</param> + /// <param name="physicalMemory">Physical memory where the texture is mapped</param> + /// <param name="info">Texture information</param> + /// <param name="sizeInfo">Size information of the texture</param> + /// <param name="range">Physical memory ranges where the texture data is located</param> + private void InitializeTexture( + GpuContext context, + PhysicalMemory physicalMemory, + TextureInfo info, + SizeInfo sizeInfo, + MultiRange range) + { + _context = context; + _physicalMemory = physicalMemory; + _sizeInfo = sizeInfo; + Range = range; + + SetInfo(info); + + _viewStorage = this; + + _views = new List<Texture>(); + _poolOwners = new List<TexturePoolOwner>(); + } + + /// <summary> + /// Initializes the data for a texture. Can optionally initialize the texture with or without data. + /// If the texture is a view, it will initialize memory tracking to be non-dirty. + /// </summary> + /// <param name="isView">True if the texture is a view, false otherwise</param> + /// <param name="withData">True if the texture is to be initialized with data</param> + public void InitializeData(bool isView, bool withData = false) + { + withData |= Group != null && Group.FlushIncompatibleOverlapsIfNeeded(); + + if (withData) + { + Debug.Assert(!isView); + + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor); + HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor); + + SynchronizeMemory(); // Load the data. + if (ScaleMode == TextureScaleMode.Scaled) + { + SetScale(GraphicsConfig.ResScale); // Scale the data up. + } + } + else + { + _hasData = true; + + if (!isView) + { + // Don't update this texture the next time we synchronize. + CheckModified(true); + + if (ScaleMode == TextureScaleMode.Scaled) + { + // Don't need to start at 1x as there is no data to scale, just go straight to the target scale. + ScaleFactor = GraphicsConfig.ResScale; + } + + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, ScaleFactor); + HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor); + } + } + } + + /// <summary> + /// Initialize a new texture group with this texture as storage. + /// </summary> + /// <param name="hasLayerViews">True if the texture will have layer views</param> + /// <param name="hasMipViews">True if the texture will have mip views</param> + /// <param name="incompatibleOverlaps">Groups that overlap with this one but are incompatible</param> + public void InitializeGroup(bool hasLayerViews, bool hasMipViews, List<TextureIncompatibleOverlap> incompatibleOverlaps) + { + Group = new TextureGroup(_context, _physicalMemory, this, incompatibleOverlaps); + + Group.Initialize(ref _sizeInfo, hasLayerViews, hasMipViews); + } + + /// <summary> + /// Create a texture view from this texture. + /// A texture view is defined as a child texture, from a sub-range of their parent texture. + /// For example, the initial layer and mipmap level of the view can be defined, so the texture + /// will start at the given layer/level of the parent texture. + /// </summary> + /// <param name="info">Child texture information</param> + /// <param name="sizeInfo">Child texture size information</param> + /// <param name="range">Physical memory ranges where the texture data is located</param> + /// <param name="firstLayer">Start layer of the child texture on the parent texture</param> + /// <param name="firstLevel">Start mipmap level of the child texture on the parent texture</param> + /// <returns>The child texture</returns> + public Texture CreateView(TextureInfo info, SizeInfo sizeInfo, MultiRange range, int firstLayer, int firstLevel) + { + Texture texture = new Texture( + _context, + _physicalMemory, + info, + sizeInfo, + range, + FirstLayer + firstLayer, + FirstLevel + firstLevel, + ScaleFactor, + ScaleMode); + + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(info, _context.Capabilities, ScaleFactor); + texture.HostTexture = HostTexture.CreateView(createInfo, firstLayer, firstLevel); + + _viewStorage.AddView(texture); + + return texture; + } + + /// <summary> + /// Adds a child texture to this texture. + /// </summary> + /// <param name="texture">The child texture</param> + private void AddView(Texture texture) + { + IncrementReferenceCount(); + + _views.Add(texture); + + texture._viewStorage = this; + + Group.UpdateViews(_views, texture); + + if (texture.Group != null && texture.Group != Group) + { + if (texture.Group.Storage == texture) + { + // This texture's group is no longer used. + Group.Inherit(texture.Group); + + texture.Group.Dispose(); + } + } + + texture.Group = Group; + } + + /// <summary> + /// Removes a child texture from this texture. + /// </summary> + /// <param name="texture">The child texture</param> + private void RemoveView(Texture texture) + { + _views.Remove(texture); + + Group.RemoveView(texture); + + texture._viewStorage = texture; + + DecrementReferenceCount(); + } + + /// <summary> + /// Replaces the texture's physical memory range. This forces tracking to regenerate. + /// </summary> + /// <param name="range">New physical memory range backing the texture</param> + public void ReplaceRange(MultiRange range) + { + Range = range; + + Group.RangeChanged(); + } + + /// <summary> + /// Create a copy dependency to a texture that is view compatible with this one. + /// When either texture is modified, the texture data will be copied to the other to keep them in sync. + /// This is essentially an emulated view, useful for handling multiple view parents or format incompatibility. + /// This also forces a copy on creation, to or from the given texture to get them in sync immediately. + /// </summary> + /// <param name="contained">The view compatible texture to create a dependency to</param> + /// <param name="layer">The base layer of the given texture relative to this one</param> + /// <param name="level">The base level of the given texture relative to this one</param> + /// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param> + public void CreateCopyDependency(Texture contained, int layer, int level, bool copyTo) + { + if (contained.Group == Group) + { + return; + } + + Group.CreateCopyDependency(contained, FirstLayer + layer, FirstLevel + level, copyTo); + } + + /// <summary> + /// Registers when a texture has had its data set after being scaled, and + /// determines if it should be blacklisted from scaling to improve performance. + /// </summary> + /// <returns>True if setting data for a scaled texture is allowed, false if the texture has been blacklisted</returns> + private bool AllowScaledSetData() + { + _scaledSetScore += ScaledSetWeight; + + if (_scaledSetScore >= ScaledSetThreshold) + { + BlacklistScale(); + + return false; + } + + return true; + } + + /// <summary> + /// Blacklists this texture from being scaled. Resets its scale to 1 if needed. + /// </summary> + public void BlacklistScale() + { + ScaleMode = TextureScaleMode.Blacklisted; + SetScale(1f); + } + + /// <summary> + /// Propagates the scale between this texture and another to ensure they have the same scale. + /// If one texture is blacklisted from scaling, the other will become blacklisted too. + /// </summary> + /// <param name="other">The other texture</param> + public void PropagateScale(Texture other) + { + if (other.ScaleMode == TextureScaleMode.Blacklisted || ScaleMode == TextureScaleMode.Blacklisted) + { + BlacklistScale(); + other.BlacklistScale(); + } + else + { + // Prefer the configured scale if present. If not, prefer the max. + float targetScale = GraphicsConfig.ResScale; + float sharedScale = (ScaleFactor == targetScale || other.ScaleFactor == targetScale) ? targetScale : Math.Max(ScaleFactor, other.ScaleFactor); + + SetScale(sharedScale); + other.SetScale(sharedScale); + } + } + + /// <summary> + /// Copy the host texture to a scaled one. If a texture is not provided, create it with the given scale. + /// </summary> + /// <param name="scale">Scale factor</param> + /// <param name="copy">True if the data should be copied to the texture, false otherwise</param> + /// <param name="storage">Texture to use instead of creating one</param> + /// <returns>A host texture containing a scaled version of this texture</returns> + private ITexture GetScaledHostTexture(float scale, bool copy, ITexture storage = null) + { + if (storage == null) + { + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(Info, _context.Capabilities, scale); + storage = _context.Renderer.CreateTexture(createInfo, scale); + } + + if (copy) + { + HostTexture.CopyTo(storage, new Extents2D(0, 0, HostTexture.Width, HostTexture.Height), new Extents2D(0, 0, storage.Width, storage.Height), true); + } + + return storage; + } + + /// <summary> + /// Sets the Scale Factor on this texture, and immediately recreates it at the correct size. + /// When a texture is resized, a scaled copy is performed from the old texture to the new one, to ensure no data is lost. + /// If scale is equivalent, this only propagates the blacklisted/scaled mode. + /// If called on a view, its storage is resized instead. + /// When resizing storage, all texture views are recreated. + /// </summary> + /// <param name="scale">The new scale factor for this texture</param> + public void SetScale(float scale) + { + bool unscaled = ScaleMode == TextureScaleMode.Blacklisted || (ScaleMode == TextureScaleMode.Undesired && scale == 1); + TextureScaleMode newScaleMode = unscaled ? ScaleMode : TextureScaleMode.Scaled; + + if (_viewStorage != this) + { + _viewStorage.ScaleMode = newScaleMode; + _viewStorage.SetScale(scale); + return; + } + + if (ScaleFactor != scale) + { + Logger.Debug?.Print(LogClass.Gpu, $"Rescaling {Info.Width}x{Info.Height} {Info.FormatInfo.Format.ToString()} to ({ScaleFactor} to {scale}). "); + + ScaleFactor = scale; + + ITexture newStorage = GetScaledHostTexture(ScaleFactor, true); + + Logger.Debug?.Print(LogClass.Gpu, $" Copy performed: {HostTexture.Width}x{HostTexture.Height} to {newStorage.Width}x{newStorage.Height}"); + + ReplaceStorage(newStorage); + + // All views must be recreated against the new storage. + + foreach (var view in _views) + { + Logger.Debug?.Print(LogClass.Gpu, $" Recreating view {Info.Width}x{Info.Height} {Info.FormatInfo.Format.ToString()}."); + view.ScaleFactor = scale; + + TextureCreateInfo viewCreateInfo = TextureCache.GetCreateInfo(view.Info, _context.Capabilities, scale); + ITexture newView = HostTexture.CreateView(viewCreateInfo, view.FirstLayer - FirstLayer, view.FirstLevel - FirstLevel); + + view.ReplaceStorage(newView); + view.ScaleMode = newScaleMode; + } + } + + if (ScaleMode != newScaleMode) + { + ScaleMode = newScaleMode; + + foreach (var view in _views) + { + view.ScaleMode = newScaleMode; + } + } + } + + /// <summary> + /// Checks if the memory for this texture was modified, and returns true if it was. + /// The modified flags are optionally consumed as a result. + /// </summary> + /// <param name="consume">True to consume the dirty flags and reprotect, false to leave them as is</param> + /// <returns>True if the texture was modified, false otherwise.</returns> + public bool CheckModified(bool consume) + { + return Group.CheckDirty(this, consume); + } + + /// <summary> + /// Synchronizes guest and host memory. + /// This will overwrite the texture data with the texture data on the guest memory, if a CPU + /// modification is detected. + /// Be aware that this can cause texture data written by the GPU to be lost, this is just a + /// one way copy (from CPU owned to GPU owned memory). + /// </summary> + public void SynchronizeMemory() + { + if (Target == Target.TextureBuffer) + { + return; + } + + if (!_dirty) + { + return; + } + + _dirty = false; + + if (_hasData) + { + Group.SynchronizeMemory(this); + } + else + { + Group.CheckDirty(this, true); + SynchronizeFull(); + } + } + + /// <summary> + /// Signal that this texture is dirty, indicating that the texture group must be checked. + /// </summary> + public void SignalGroupDirty() + { + _dirty = true; + } + + /// <summary> + /// Signal that the modified state is dirty, indicating that the texture group should be notified when it changes. + /// </summary> + public void SignalModifiedDirty() + { + _modifiedStale = true; + } + + /// <summary> + /// Fully synchronizes guest and host memory. + /// This will replace the entire texture with the data present in guest memory. + /// </summary> + public void SynchronizeFull() + { + ReadOnlySpan<byte> data = _physicalMemory.GetSpan(Range); + + // If the host does not support ASTC compression, we need to do the decompression. + // The decompression is slow, so we want to avoid it as much as possible. + // This does a byte-by-byte check and skips the update if the data is equal in this case. + // This improves the speed on applications that overwrites ASTC data without changing anything. + if (Info.FormatInfo.Format.IsAstc() && !_context.Capabilities.SupportsAstcCompression) + { + if (_updateCount < ByteComparisonSwitchThreshold) + { + _updateCount++; + } + else + { + bool dataMatches = _currentData != null && data.SequenceEqual(_currentData); + if (dataMatches) + { + return; + } + + _currentData = data.ToArray(); + } + } + + SpanOrArray<byte> result = ConvertToHostCompatibleFormat(data); + + if (ScaleFactor != 1f && AllowScaledSetData()) + { + // If needed, create a texture to load from 1x scale. + ITexture texture = _setHostTexture = GetScaledHostTexture(1f, false, _setHostTexture); + + texture.SetData(result); + + texture.CopyTo(HostTexture, new Extents2D(0, 0, texture.Width, texture.Height), new Extents2D(0, 0, HostTexture.Width, HostTexture.Height), true); + } + else + { + HostTexture.SetData(result); + } + + _hasData = true; + } + + /// <summary> + /// Uploads new texture data to the host GPU. + /// </summary> + /// <param name="data">New data</param> + public void SetData(SpanOrArray<byte> data) + { + BlacklistScale(); + + Group.CheckDirty(this, true); + + AlwaysFlushOnOverlap = true; + + HostTexture.SetData(data); + + _hasData = true; + } + + /// <summary> + /// Uploads new texture data to the host GPU for a specific layer/level. + /// </summary> + /// <param name="data">New data</param> + /// <param name="layer">Target layer</param> + /// <param name="level">Target level</param> + public void SetData(SpanOrArray<byte> data, int layer, int level) + { + BlacklistScale(); + + HostTexture.SetData(data, layer, level); + + _currentData = null; + + _hasData = true; + } + + /// <summary> + /// Uploads new texture data to the host GPU for a specific layer/level and 2D sub-region. + /// </summary> + /// <param name="data">New data</param> + /// <param name="layer">Target layer</param> + /// <param name="level">Target level</param> + /// <param name="region">Target sub-region of the texture to update</param> + public void SetData(ReadOnlySpan<byte> data, int layer, int level, Rectangle<int> region) + { + BlacklistScale(); + + HostTexture.SetData(data, layer, level, region); + + _currentData = null; + + _hasData = true; + } + + /// <summary> + /// Converts texture data to a format and layout that is supported by the host GPU. + /// </summary> + /// <param name="data">Data to be converted</param> + /// <param name="level">Mip level to convert</param> + /// <param name="single">True to convert a single slice</param> + /// <returns>Converted data</returns> + public SpanOrArray<byte> ConvertToHostCompatibleFormat(ReadOnlySpan<byte> data, int level = 0, bool single = false) + { + int width = Info.Width; + int height = Info.Height; + + int depth = _depth; + int layers = single ? 1 : _layers; + int levels = single ? 1 : (Info.Levels - level); + + width = Math.Max(width >> level, 1); + height = Math.Max(height >> level, 1); + depth = Math.Max(depth >> level, 1); + + int sliceDepth = single ? 1 : depth; + + SpanOrArray<byte> result; + + if (Info.IsLinear) + { + result = LayoutConverter.ConvertLinearStridedToLinear( + width, + height, + Info.FormatInfo.BlockWidth, + Info.FormatInfo.BlockHeight, + Info.Stride, + Info.Stride, + Info.FormatInfo.BytesPerPixel, + data); + } + else + { + result = LayoutConverter.ConvertBlockLinearToLinear( + width, + height, + depth, + sliceDepth, + levels, + layers, + Info.FormatInfo.BlockWidth, + Info.FormatInfo.BlockHeight, + Info.FormatInfo.BytesPerPixel, + Info.GobBlocksInY, + Info.GobBlocksInZ, + Info.GobBlocksInTileX, + _sizeInfo, + data); + } + + // Handle compressed cases not supported by the host: + // - ASTC is usually not supported on desktop cards. + // - BC4/BC5 is not supported on 3D textures. + if (!_context.Capabilities.SupportsAstcCompression && Format.IsAstc()) + { + if (!AstcDecoder.TryDecodeToRgba8P( + result.ToArray(), + Info.FormatInfo.BlockWidth, + Info.FormatInfo.BlockHeight, + width, + height, + sliceDepth, + levels, + layers, + out byte[] decoded)) + { + string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}"; + + Logger.Debug?.Print(LogClass.Gpu, $"Invalid ASTC texture at 0x{Info.GpuAddress:X} ({texInfo})."); + } + + if (GraphicsConfig.EnableTextureRecompression) + { + decoded = BCnEncoder.EncodeBC7(decoded, width, height, sliceDepth, levels, layers); + } + + result = decoded; + } + else if (!_context.Capabilities.SupportsEtc2Compression && Format.IsEtc2()) + { + switch (Format) + { + case Format.Etc2RgbaSrgb: + case Format.Etc2RgbaUnorm: + result = ETC2Decoder.DecodeRgba(result, width, height, sliceDepth, levels, layers); + break; + case Format.Etc2RgbPtaSrgb: + case Format.Etc2RgbPtaUnorm: + result = ETC2Decoder.DecodePta(result, width, height, sliceDepth, levels, layers); + break; + case Format.Etc2RgbSrgb: + case Format.Etc2RgbUnorm: + result = ETC2Decoder.DecodeRgb(result, width, height, sliceDepth, levels, layers); + break; + } + } + else if (!TextureCompatibility.HostSupportsBcFormat(Format, Target, _context.Capabilities)) + { + switch (Format) + { + case Format.Bc1RgbaSrgb: + case Format.Bc1RgbaUnorm: + result = BCnDecoder.DecodeBC1(result, width, height, sliceDepth, levels, layers); + break; + case Format.Bc2Srgb: + case Format.Bc2Unorm: + result = BCnDecoder.DecodeBC2(result, width, height, sliceDepth, levels, layers); + break; + case Format.Bc3Srgb: + case Format.Bc3Unorm: + result = BCnDecoder.DecodeBC3(result, width, height, sliceDepth, levels, layers); + break; + case Format.Bc4Snorm: + case Format.Bc4Unorm: + result = BCnDecoder.DecodeBC4(result, width, height, sliceDepth, levels, layers, Format == Format.Bc4Snorm); + break; + case Format.Bc5Snorm: + case Format.Bc5Unorm: + result = BCnDecoder.DecodeBC5(result, width, height, sliceDepth, levels, layers, Format == Format.Bc5Snorm); + break; + case Format.Bc6HSfloat: + case Format.Bc6HUfloat: + result = BCnDecoder.DecodeBC6(result, width, height, sliceDepth, levels, layers, Format == Format.Bc6HSfloat); + break; + case Format.Bc7Srgb: + case Format.Bc7Unorm: + result = BCnDecoder.DecodeBC7(result, width, height, sliceDepth, levels, layers); + break; + } + } + else if (!_context.Capabilities.SupportsR4G4Format && Format == Format.R4G4Unorm) + { + result = PixelConverter.ConvertR4G4ToR4G4B4A4(result, width); + + if (!_context.Capabilities.SupportsR4G4B4A4Format) + { + result = PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result, width); + } + } + else if (Format == Format.R4G4B4A4Unorm) + { + if (!_context.Capabilities.SupportsR4G4B4A4Format) + { + result = PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result, width); + } + } + else if (!_context.Capabilities.Supports5BitComponentFormat && Format.Is16BitPacked()) + { + switch (Format) + { + case Format.B5G6R5Unorm: + case Format.R5G6B5Unorm: + result = PixelConverter.ConvertR5G6B5ToR8G8B8A8(result, width); + break; + case Format.B5G5R5A1Unorm: + case Format.R5G5B5X1Unorm: + case Format.R5G5B5A1Unorm: + result = PixelConverter.ConvertR5G5B5ToR8G8B8A8(result, width, Format == Format.R5G5B5X1Unorm); + break; + case Format.A1B5G5R5Unorm: + result = PixelConverter.ConvertA1B5G5R5ToR8G8B8A8(result, width); + break; + case Format.R4G4B4A4Unorm: + result = PixelConverter.ConvertR4G4B4A4ToR8G8B8A8(result, width); + break; + } + } + + return result; + } + + /// <summary> + /// Converts texture data from a format and layout that is supported by the host GPU, back into the intended format on the guest GPU. + /// </summary> + /// <param name="output">Optional output span to convert into</param> + /// <param name="data">Data to be converted</param> + /// <param name="level">Mip level to convert</param> + /// <param name="single">True to convert a single slice</param> + /// <returns>Converted data</returns> + public ReadOnlySpan<byte> ConvertFromHostCompatibleFormat(Span<byte> output, ReadOnlySpan<byte> data, int level = 0, bool single = false) + { + if (Target != Target.TextureBuffer) + { + int width = Info.Width; + int height = Info.Height; + + int depth = _depth; + int layers = single ? 1 : _layers; + int levels = single ? 1 : (Info.Levels - level); + + width = Math.Max(width >> level, 1); + height = Math.Max(height >> level, 1); + depth = Math.Max(depth >> level, 1); + + if (Info.IsLinear) + { + data = LayoutConverter.ConvertLinearToLinearStrided( + output, + Info.Width, + Info.Height, + Info.FormatInfo.BlockWidth, + Info.FormatInfo.BlockHeight, + Info.Stride, + Info.FormatInfo.BytesPerPixel, + data); + } + else + { + data = LayoutConverter.ConvertLinearToBlockLinear( + output, + width, + height, + depth, + single ? 1 : depth, + levels, + layers, + Info.FormatInfo.BlockWidth, + Info.FormatInfo.BlockHeight, + Info.FormatInfo.BytesPerPixel, + Info.GobBlocksInY, + Info.GobBlocksInZ, + Info.GobBlocksInTileX, + _sizeInfo, + data); + } + } + + return data; + } + + /// <summary> + /// Flushes the texture data. + /// This causes the texture data to be written back to guest memory. + /// If the texture was written by the GPU, this includes all modification made by the GPU + /// up to this point. + /// Be aware that this is an expensive operation, avoid calling it unless strictly needed. + /// This may cause data corruption if the memory is already being used for something else on the CPU side. + /// </summary> + /// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param> + /// <returns>True if data was flushed, false otherwise</returns> + public bool FlushModified(bool tracked = true) + { + return TextureCompatibility.CanTextureFlush(Info, _context.Capabilities) && Group.FlushModified(this, tracked); + } + + /// <summary> + /// Flushes the texture data. + /// This causes the texture data to be written back to guest memory. + /// If the texture was written by the GPU, this includes all modification made by the GPU + /// up to this point. + /// Be aware that this is an expensive operation, avoid calling it unless strictly needed. + /// This may cause data corruption if the memory is already being used for something else on the CPU side. + /// </summary> + /// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param> + public void Flush(bool tracked) + { + if (TextureCompatibility.CanTextureFlush(Info, _context.Capabilities)) + { + FlushTextureDataToGuest(tracked); + } + } + + /// <summary> + /// Gets a host texture to use for flushing the texture, at 1x resolution. + /// If the HostTexture is already at 1x resolution, it is returned directly. + /// </summary> + /// <returns>The host texture to flush</returns> + public ITexture GetFlushTexture() + { + ITexture texture = HostTexture; + if (ScaleFactor != 1f) + { + // If needed, create a texture to flush back to host at 1x scale. + texture = _flushHostTexture = GetScaledHostTexture(1f, true, _flushHostTexture); + } + + return texture; + } + + /// <summary> + /// Gets data from the host GPU, and flushes it all to guest memory. + /// </summary> + /// <remarks> + /// This method should be used to retrieve data that was modified by the host GPU. + /// This is not cheap, avoid doing that unless strictly needed. + /// When possible, the data is written directly into guest memory, rather than copied. + /// </remarks> + /// <param name="tracked">True if writing the texture data is tracked, false otherwise</param> + /// <param name="texture">The specific host texture to flush. Defaults to this texture</param> + public void FlushTextureDataToGuest(bool tracked, ITexture texture = null) + { + using WritableRegion region = _physicalMemory.GetWritableRegion(Range, tracked); + + GetTextureDataFromGpu(region.Memory.Span, tracked, texture); + } + + /// <summary> + /// Gets data from the host GPU. + /// </summary> + /// <remarks> + /// This method should be used to retrieve data that was modified by the host GPU. + /// This is not cheap, avoid doing that unless strictly needed. + /// </remarks> + /// <param name="output">An output span to place the texture data into</param> + /// <param name="blacklist">True if the texture should be blacklisted, false otherwise</param> + /// <param name="texture">The specific host texture to flush. Defaults to this texture</param> + private void GetTextureDataFromGpu(Span<byte> output, bool blacklist, ITexture texture = null) + { + PinnedSpan<byte> data; + + if (texture != null) + { + data = texture.GetData(); + } + else + { + if (blacklist) + { + BlacklistScale(); + data = HostTexture.GetData(); + } + else if (ScaleFactor != 1f) + { + float scale = ScaleFactor; + SetScale(1f); + data = HostTexture.GetData(); + SetScale(scale); + } + else + { + data = HostTexture.GetData(); + } + } + + ConvertFromHostCompatibleFormat(output, data.Get()); + + data.Dispose(); + } + + /// <summary> + /// Gets data from the host GPU for a single slice. + /// </summary> + /// <remarks> + /// This method should be used to retrieve data that was modified by the host GPU. + /// This is not cheap, avoid doing that unless strictly needed. + /// </remarks> + /// <param name="output">An output span to place the texture data into. If empty, one is generated</param> + /// <param name="layer">The layer of the texture to flush</param> + /// <param name="level">The level of the texture to flush</param> + /// <param name="blacklist">True if the texture should be blacklisted, false otherwise</param> + /// <param name="texture">The specific host texture to flush. Defaults to this texture</param> + public void GetTextureDataSliceFromGpu(Span<byte> output, int layer, int level, bool blacklist, ITexture texture = null) + { + PinnedSpan<byte> data; + + if (texture != null) + { + data = texture.GetData(layer, level); + } + else + { + if (blacklist) + { + BlacklistScale(); + data = HostTexture.GetData(layer, level); + } + else if (ScaleFactor != 1f) + { + float scale = ScaleFactor; + SetScale(1f); + data = HostTexture.GetData(layer, level); + SetScale(scale); + } + else + { + data = HostTexture.GetData(layer, level); + } + } + + ConvertFromHostCompatibleFormat(output, data.Get(), level, true); + + data.Dispose(); + } + + /// <summary> + /// This performs a strict comparison, used to check if this texture is equal to the one supplied. + /// </summary> + /// <param name="info">Texture information to compare against</param> + /// <param name="flags">Comparison flags</param> + /// <returns>A value indicating how well this texture matches the given info</returns> + public TextureMatchQuality IsExactMatch(TextureInfo info, TextureSearchFlags flags) + { + bool forSampler = (flags & TextureSearchFlags.ForSampler) != 0; + + TextureMatchQuality matchQuality = TextureCompatibility.FormatMatches(Info, info, forSampler, (flags & TextureSearchFlags.ForCopy) != 0); + + if (matchQuality == TextureMatchQuality.NoMatch) + { + return matchQuality; + } + + if (!TextureCompatibility.LayoutMatches(Info, info)) + { + return TextureMatchQuality.NoMatch; + } + + if (!TextureCompatibility.SizeMatches(Info, info, forSampler)) + { + return TextureMatchQuality.NoMatch; + } + + if ((flags & TextureSearchFlags.ForSampler) != 0) + { + if (!TextureCompatibility.SamplerParamsMatches(Info, info)) + { + return TextureMatchQuality.NoMatch; + } + } + + if ((flags & TextureSearchFlags.ForCopy) != 0) + { + bool msTargetCompatible = Info.Target == Target.Texture2DMultisample && info.Target == Target.Texture2D; + + if (!msTargetCompatible && !TextureCompatibility.TargetAndSamplesCompatible(Info, info)) + { + return TextureMatchQuality.NoMatch; + } + } + else if (!TextureCompatibility.TargetAndSamplesCompatible(Info, info)) + { + return TextureMatchQuality.NoMatch; + } + + return Info.Levels == info.Levels ? matchQuality : TextureMatchQuality.NoMatch; + } + + /// <summary> + /// Check if it's possible to create a view, with the given parameters, from this texture. + /// </summary> + /// <param name="info">Texture view information</param> + /// <param name="range">Texture view physical memory ranges</param> + /// <param name="exactSize">Indicates if the texture sizes must be exactly equal, or width is allowed to differ</param> + /// <param name="layerSize">Layer size on the given texture</param> + /// <param name="caps">Host GPU capabilities</param> + /// <param name="firstLayer">Texture view initial layer on this texture</param> + /// <param name="firstLevel">Texture view first mipmap level on this texture</param> + /// <returns>The level of compatiblilty a view with the given parameters created from this texture has</returns> + public TextureViewCompatibility IsViewCompatible( + TextureInfo info, + MultiRange range, + bool exactSize, + int layerSize, + Capabilities caps, + out int firstLayer, + out int firstLevel) + { + TextureViewCompatibility result = TextureViewCompatibility.Full; + + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewFormatCompatible(Info, info, caps)); + if (result != TextureViewCompatibility.Incompatible) + { + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info, ref caps)); + + bool bothMs = Info.Target.IsMultisample() && info.Target.IsMultisample(); + if (bothMs && (Info.SamplesInX != info.SamplesInX || Info.SamplesInY != info.SamplesInY)) + { + result = TextureViewCompatibility.Incompatible; + } + + if (result == TextureViewCompatibility.Full && Info.FormatInfo.Format != info.FormatInfo.Format && !_context.Capabilities.SupportsMismatchingViewFormat) + { + // AMD and Intel have a bug where the view format is always ignored; + // they use the parent format instead. + // Create a copy dependency to avoid this issue. + + result = TextureViewCompatibility.CopyOnly; + } + } + + firstLayer = 0; + firstLevel = 0; + + if (result == TextureViewCompatibility.Incompatible) + { + return TextureViewCompatibility.Incompatible; + } + + int offset = Range.FindOffset(range); + + if (offset < 0 || !_sizeInfo.FindView(offset, out firstLayer, out firstLevel)) + { + return TextureViewCompatibility.LayoutIncompatible; + } + + if (!TextureCompatibility.ViewLayoutCompatible(Info, info, firstLevel)) + { + return TextureViewCompatibility.LayoutIncompatible; + } + + if (info.GetSlices() > 1 && LayerSize != layerSize) + { + return TextureViewCompatibility.LayoutIncompatible; + } + + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSizeMatches(Info, info, exactSize, firstLevel)); + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSubImagesInBounds(Info, info, firstLayer, firstLevel)); + + return result; + } + + /// <summary> + /// Gets a texture of the specified target type from this texture. + /// This can be used to get an array texture from a non-array texture and vice-versa. + /// If this texture and the requested targets are equal, then this texture Host texture is returned directly. + /// </summary> + /// <param name="target">The desired target type</param> + /// <returns>A view of this texture with the requested target, or null if the target is invalid for this texture</returns> + public ITexture GetTargetTexture(Target target) + { + if (target == Target) + { + return HostTexture; + } + + if (_arrayViewTexture == null && IsSameDimensionsTarget(target)) + { + FormatInfo formatInfo = TextureCompatibility.ToHostCompatibleFormat(Info, _context.Capabilities); + + TextureCreateInfo createInfo = new TextureCreateInfo( + Info.Width, + Info.Height, + target == Target.CubemapArray ? 6 : 1, + Info.Levels, + Info.Samples, + formatInfo.BlockWidth, + formatInfo.BlockHeight, + formatInfo.BytesPerPixel, + formatInfo.Format, + Info.DepthStencilMode, + target, + Info.SwizzleR, + Info.SwizzleG, + Info.SwizzleB, + Info.SwizzleA); + + ITexture viewTexture = HostTexture.CreateView(createInfo, 0, 0); + + _arrayViewTexture = viewTexture; + _arrayViewTarget = target; + + return viewTexture; + } + else if (_arrayViewTarget == target) + { + return _arrayViewTexture; + } + + return null; + } + + /// <summary> + /// Determine if this texture can have anisotropic filtering forced. + /// Filtered textures that we might want to force anisotropy on should have a lot of mip levels. + /// </summary> + /// <returns>True if anisotropic filtering can be forced, false otherwise</returns> + private bool CanTextureForceAnisotropy() + { + if (!(Target == Target.Texture2D || Target == Target.Texture2DArray)) + { + return false; + } + + int maxSize = Math.Max(Info.Width, Info.Height); + int maxLevels = BitOperations.Log2((uint)maxSize) + 1; + + return Info.Levels >= Math.Min(MinLevelsForForceAnisotropy, maxLevels); + } + + /// <summary> + /// Check if this texture and the specified target have the same number of dimensions. + /// For the purposes of this comparison, 2D and 2D Multisample textures are not considered to have + /// the same number of dimensions. Same for Cubemap and 3D textures. + /// </summary> + /// <param name="target">The target to compare with</param> + /// <returns>True if both targets have the same number of dimensions, false otherwise</returns> + private bool IsSameDimensionsTarget(Target target) + { + switch (Info.Target) + { + case Target.Texture1D: + case Target.Texture1DArray: + return target == Target.Texture1D || + target == Target.Texture1DArray; + + case Target.Texture2D: + case Target.Texture2DArray: + return target == Target.Texture2D || + target == Target.Texture2DArray; + + case Target.Cubemap: + case Target.CubemapArray: + return target == Target.Cubemap || + target == Target.CubemapArray; + + case Target.Texture2DMultisample: + case Target.Texture2DMultisampleArray: + return target == Target.Texture2DMultisample || + target == Target.Texture2DMultisampleArray; + + case Target.Texture3D: + return target == Target.Texture3D; + } + + return false; + } + + /// <summary> + /// Replaces view texture information. + /// This should only be used for child textures with a parent. + /// </summary> + /// <param name="parent">The parent texture</param> + /// <param name="info">The new view texture information</param> + /// <param name="hostTexture">The new host texture</param> + /// <param name="firstLayer">The first layer of the view</param> + /// <param name="firstLevel">The first level of the view</param> + public void ReplaceView(Texture parent, TextureInfo info, ITexture hostTexture, int firstLayer, int firstLevel) + { + IncrementReferenceCount(); + parent._viewStorage.SynchronizeMemory(); + + // If this texture has views, they must be given to the new parent. + if (_views.Count > 0) + { + Texture[] viewCopy = _views.ToArray(); + + foreach (Texture view in viewCopy) + { + TextureCreateInfo createInfo = TextureCache.GetCreateInfo(view.Info, _context.Capabilities, ScaleFactor); + + ITexture newView = parent.HostTexture.CreateView(createInfo, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel); + + view.ReplaceView(parent, view.Info, newView, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel); + } + } + + ReplaceStorage(hostTexture); + + if (_viewStorage != this) + { + _viewStorage.RemoveView(this); + } + + FirstLayer = parent.FirstLayer + firstLayer; + FirstLevel = parent.FirstLevel + firstLevel; + parent._viewStorage.AddView(this); + + SetInfo(info); + DecrementReferenceCount(); + } + + /// <summary> + /// Sets the internal texture information structure. + /// </summary> + /// <param name="info">The new texture information</param> + private void SetInfo(TextureInfo info) + { + Info = info; + Target = info.Target; + Width = info.Width; + Height = info.Height; + CanForceAnisotropy = CanTextureForceAnisotropy(); + + _depth = info.GetDepth(); + _layers = info.GetLayers(); + } + + /// <summary> + /// Signals that the texture has been modified. + /// </summary> + public void SignalModified() + { + _scaledSetScore = Math.Max(0, _scaledSetScore - 1); + + if (_modifiedStale || Group.HasCopyDependencies) + { + _modifiedStale = false; + Group.SignalModified(this); + } + + _physicalMemory.TextureCache.Lift(this); + } + + /// <summary> + /// Signals that a texture has been bound, or has been unbound. + /// During this time, lazy copies will not clear the dirty flag. + /// </summary> + /// <param name="bound">True if the texture has been bound, false if it has been unbound</param> + public void SignalModifying(bool bound) + { + if (bound) + { + _scaledSetScore = Math.Max(0, _scaledSetScore - 1); + } + + if (_modifiedStale || Group.HasCopyDependencies) + { + _modifiedStale = false; + Group.SignalModifying(this, bound); + } + + _physicalMemory.TextureCache.Lift(this); + + if (bound) + { + IncrementReferenceCount(); + } + else + { + DecrementReferenceCount(); + } + } + + /// <summary> + /// Replaces the host texture, while disposing of the old one if needed. + /// </summary> + /// <param name="hostTexture">The new host texture</param> + private void ReplaceStorage(ITexture hostTexture) + { + DisposeTextures(); + + HostTexture = hostTexture; + } + + /// <summary> + /// Determine if any of this texture's data overlaps with another. + /// </summary> + /// <param name="texture">The texture to check against</param> + /// <param name="compatibility">The view compatibility of the two textures</param> + /// <returns>True if any slice of the textures overlap, false otherwise</returns> + public bool DataOverlaps(Texture texture, TextureViewCompatibility compatibility) + { + if (compatibility == TextureViewCompatibility.LayoutIncompatible && Info.GobBlocksInZ > 1 && Info.GobBlocksInZ == texture.Info.GobBlocksInZ) + { + // Allow overlapping slices of layout compatible 3D textures with matching GobBlocksInZ, as they are interleaved. + return false; + } + + if (texture._sizeInfo.AllOffsets.Length == 1 && _sizeInfo.AllOffsets.Length == 1) + { + return Range.OverlapsWith(texture.Range); + } + + MultiRange otherRange = texture.Range; + + IEnumerable<MultiRange> regions = _sizeInfo.AllRegions().Select((region) => Range.Slice((ulong)region.Offset, (ulong)region.Size)); + IEnumerable<MultiRange> otherRegions = texture._sizeInfo.AllRegions().Select((region) => otherRange.Slice((ulong)region.Offset, (ulong)region.Size)); + + foreach (MultiRange region in regions) + { + foreach (MultiRange otherRegion in otherRegions) + { + if (region.OverlapsWith(otherRegion)) + { + return true; + } + } + } + + return false; + } + + /// <summary> + /// Increments the texture reference count. + /// </summary> + public void IncrementReferenceCount() + { + _referenceCount++; + } + + /// <summary> + /// Increments the reference count and records the given texture pool and ID as a pool owner. + /// </summary> + /// <param name="pool">The texture pool this texture has been added to</param> + /// <param name="id">The ID of the reference to this texture in the pool</param> + /// <param name="gpuVa">GPU VA of the pool reference</param> + public void IncrementReferenceCount(TexturePool pool, int id, ulong gpuVa) + { + lock (_poolOwners) + { + _poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id, GpuAddress = gpuVa }); + } + _referenceCount++; + + if (ShortCacheEntry != null) + { + _physicalMemory.TextureCache.RemoveShortCache(this); + } + } + + /// <summary> + /// Indicates that the texture has one reference left, and will delete on reference decrement. + /// </summary> + /// <returns>True if there is one reference remaining, false otherwise</returns> + public bool HasOneReference() + { + return _referenceCount == 1; + } + + /// <summary> + /// Decrements the texture reference count. + /// When the reference count hits zero, the texture may be deleted and can't be used anymore. + /// </summary> + /// <returns>True if the texture is now referenceless, false otherwise</returns> + public bool DecrementReferenceCount() + { + int newRefCount = --_referenceCount; + + if (newRefCount == 0) + { + if (_viewStorage != this) + { + _viewStorage.RemoveView(this); + } + + _physicalMemory.TextureCache.RemoveTextureFromCache(this); + } + + Debug.Assert(newRefCount >= 0); + + DeleteIfNotUsed(); + + return newRefCount <= 0; + } + + /// <summary> + /// Decrements the texture reference count, also removing an associated pool owner reference. + /// When the reference count hits zero, the texture may be deleted and can't be used anymore. + /// </summary> + /// <param name="pool">The texture pool this texture is being removed from</param> + /// <param name="id">The ID of the reference to this texture in the pool</param> + /// <returns>True if the texture is now referenceless, false otherwise</returns> + public bool DecrementReferenceCount(TexturePool pool, int id = -1) + { + lock (_poolOwners) + { + int references = _poolOwners.RemoveAll(entry => entry.Pool == pool && entry.ID == id || id == -1); + + if (references == 0) + { + // This reference has already been removed. + return _referenceCount <= 0; + } + + Debug.Assert(references == 1); + } + + return DecrementReferenceCount(); + } + + /// <summary> + /// Forcibly remove this texture from all pools that reference it. + /// </summary> + /// <param name="deferred">Indicates if the removal is being done from another thread.</param> + public void RemoveFromPools(bool deferred) + { + lock (_poolOwners) + { + foreach (var owner in _poolOwners) + { + owner.Pool.ForceRemove(this, owner.ID, deferred); + } + + _poolOwners.Clear(); + } + + if (ShortCacheEntry != null && _context.IsGpuThread()) + { + // If this is called from another thread (unmapped), the short cache will + // have to remove this texture on a future tick. + + _physicalMemory.TextureCache.RemoveShortCache(this); + } + + InvalidatedSequence++; + } + + /// <summary> + /// Queue updating texture mappings on the pool. Happens from another thread. + /// </summary> + public void UpdatePoolMappings() + { + lock (_poolOwners) + { + ulong address = 0; + + foreach (var owner in _poolOwners) + { + if (address == 0 || address == owner.GpuAddress) + { + address = owner.GpuAddress; + + owner.Pool.QueueUpdateMapping(this, owner.ID); + } + else + { + // If there is a different GPU VA mapping, prefer the first and delete the others. + owner.Pool.ForceRemove(this, owner.ID, true); + } + } + + _poolOwners.Clear(); + } + + InvalidatedSequence++; + } + + /// <summary> + /// Delete the texture if it is not used anymore. + /// The texture is considered unused when the reference count is zero, + /// and it has no child views. + /// </summary> + private void DeleteIfNotUsed() + { + // We can delete the texture as long it is not being used + // in any cache (the reference count is 0 in this case), and + // also all views that may be created from this texture were + // already deleted (views count is 0). + if (_referenceCount == 0 && _views.Count == 0) + { + Dispose(); + } + } + + /// <summary> + /// Performs texture disposal, deleting the texture. + /// </summary> + private void DisposeTextures() + { + InvalidatedSequence++; + + _currentData = null; + HostTexture.Release(); + + _arrayViewTexture?.Release(); + _arrayViewTexture = null; + + _flushHostTexture?.Release(); + _flushHostTexture = null; + + _setHostTexture?.Release(); + _setHostTexture = null; + } + + /// <summary> + /// Called when the memory for this texture has been unmapped. + /// Calls are from non-gpu threads. + /// </summary> + /// <param name="unmapRange">The range of memory being unmapped</param> + public void Unmapped(MultiRange unmapRange) + { + ChangedMapping = true; + + if (Group.Storage == this) + { + Group.ClearModified(unmapRange); + } + + UpdatePoolMappings(); + } + + /// <summary> + /// Performs texture disposal, deleting the texture. + /// </summary> + public void Dispose() + { + DisposeTextures(); + + if (Group.Storage == this) + { + Group.Dispose(); + } + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs new file mode 100644 index 00000000..febe508b --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs @@ -0,0 +1,73 @@ +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture binding information. + /// This is used for textures that needs to be accessed from shaders. + /// </summary> + readonly struct TextureBindingInfo + { + /// <summary> + /// Shader sampler target type. + /// </summary> + public Target Target { get; } + + /// <summary> + /// For images, indicates the format specified on the shader. + /// </summary> + public Format Format { get; } + + /// <summary> + /// Shader texture host binding point. + /// </summary> + public int Binding { get; } + + /// <summary> + /// Constant buffer slot with the texture handle. + /// </summary> + public int CbufSlot { get; } + + /// <summary> + /// Index of the texture handle on the constant buffer at slot <see cref="CbufSlot"/>. + /// </summary> + public int Handle { get; } + + /// <summary> + /// Flags from the texture descriptor that indicate how the texture is used. + /// </summary> + public TextureUsageFlags Flags { get; } + + /// <summary> + /// Constructs the texture binding information structure. + /// </summary> + /// <param name="target">The shader sampler target type</param> + /// <param name="format">Format of the image as declared on the shader</param> + /// <param name="binding">The shader texture binding point</param> + /// <param name="cbufSlot">Constant buffer slot where the texture handle is located</param> + /// <param name="handle">The shader texture handle (read index into the texture constant buffer)</param> + /// <param name="flags">The texture's usage flags, indicating how it is used in the shader</param> + public TextureBindingInfo(Target target, Format format, int binding, int cbufSlot, int handle, TextureUsageFlags flags) + { + Target = target; + Format = format; + Binding = binding; + CbufSlot = cbufSlot; + Handle = handle; + Flags = flags; + } + + /// <summary> + /// Constructs the texture binding information structure. + /// </summary> + /// <param name="target">The shader sampler target type</param> + /// <param name="binding">The shader texture binding point</param> + /// <param name="cbufSlot">Constant buffer slot where the texture handle is located</param> + /// <param name="handle">The shader texture handle (read index into the texture constant buffer)</param> + /// <param name="flags">The texture's usage flags, indicating how it is used in the shader</param> + public TextureBindingInfo(Target target, int binding, int cbufSlot, int handle, TextureUsageFlags flags) : this(target, (Format)0, binding, cbufSlot, handle, flags) + { + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs new file mode 100644 index 00000000..bbfb704d --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs @@ -0,0 +1,882 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Engine.Types; +using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Gpu.Shader; +using Ryujinx.Graphics.Shader; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture bindings manager. + /// </summary> + class TextureBindingsManager + { + private const int InitialTextureStateSize = 32; + private const int InitialImageStateSize = 8; + + private readonly GpuContext _context; + + private readonly bool _isCompute; + + private ulong _texturePoolGpuVa; + private int _texturePoolMaximumId; + private TexturePool _texturePool; + private ulong _samplerPoolGpuVa; + private int _samplerPoolMaximumId; + private SamplerIndex _samplerIndex; + private SamplerPool _samplerPool; + + private readonly GpuChannel _channel; + private readonly TexturePoolCache _texturePoolCache; + private readonly SamplerPoolCache _samplerPoolCache; + + private TexturePool _cachedTexturePool; + private SamplerPool _cachedSamplerPool; + + private TextureBindingInfo[][] _textureBindings; + private TextureBindingInfo[][] _imageBindings; + + private struct TextureState + { + public ITexture Texture; + public ISampler Sampler; + + public int TextureHandle; + public int SamplerHandle; + public Format ImageFormat; + public int InvalidatedSequence; + public Texture CachedTexture; + public Sampler CachedSampler; + } + + private TextureState[] _textureState; + private TextureState[] _imageState; + + private int _texturePoolSequence; + private int _samplerPoolSequence; + + private int _textureBufferIndex; + + private readonly float[] _scales; + private bool _scaleChanged; + private int _lastFragmentTotal; + + /// <summary> + /// Constructs a new instance of the texture bindings manager. + /// </summary> + /// <param name="context">The GPU context that the texture bindings manager belongs to</param> + /// <param name="channel">The GPU channel that the texture bindings manager belongs to</param> + /// <param name="texturePoolCache">Texture pools cache used to get texture pools from</param> + /// <param name="samplerPoolCache">Sampler pools cache used to get sampler pools from</param> + /// <param name="scales">Array where the scales for the currently bound textures are stored</param> + /// <param name="isCompute">True if the bindings manager is used for the compute engine</param> + public TextureBindingsManager( + GpuContext context, + GpuChannel channel, + TexturePoolCache texturePoolCache, + SamplerPoolCache samplerPoolCache, + float[] scales, + bool isCompute) + { + _context = context; + _channel = channel; + _texturePoolCache = texturePoolCache; + _samplerPoolCache = samplerPoolCache; + + _scales = scales; + _isCompute = isCompute; + + int stages = isCompute ? 1 : Constants.ShaderStages; + + _textureBindings = new TextureBindingInfo[stages][]; + _imageBindings = new TextureBindingInfo[stages][]; + + _textureState = new TextureState[InitialTextureStateSize]; + _imageState = new TextureState[InitialImageStateSize]; + + for (int stage = 0; stage < stages; stage++) + { + _textureBindings[stage] = new TextureBindingInfo[InitialTextureStateSize]; + _imageBindings[stage] = new TextureBindingInfo[InitialImageStateSize]; + } + } + + /// <summary> + /// Sets the texture and image bindings. + /// </summary> + /// <param name="bindings">Bindings for the active shader</param> + public void SetBindings(CachedShaderBindings bindings) + { + _textureBindings = bindings.TextureBindings; + _imageBindings = bindings.ImageBindings; + + SetMaxBindings(bindings.MaxTextureBinding, bindings.MaxImageBinding); + } + + /// <summary> + /// Sets the max binding indexes for textures and images. + /// </summary> + /// <param name="maxTextureBinding">The maximum texture binding</param> + /// <param name="maxImageBinding">The maximum image binding</param> + public void SetMaxBindings(int maxTextureBinding, int maxImageBinding) + { + if (maxTextureBinding >= _textureState.Length) + { + Array.Resize(ref _textureState, maxTextureBinding + 1); + } + + if (maxImageBinding >= _imageState.Length) + { + Array.Resize(ref _imageState, maxImageBinding + 1); + } + } + + /// <summary> + /// Sets the textures constant buffer index. + /// The constant buffer specified holds the texture handles. + /// </summary> + /// <param name="index">Constant buffer index</param> + public void SetTextureBufferIndex(int index) + { + _textureBufferIndex = index; + } + + /// <summary> + /// Sets the current texture sampler pool to be used. + /// </summary> + /// <param name="gpuVa">Start GPU virtual address of the pool</param> + /// <param name="maximumId">Maximum ID of the pool (total count minus one)</param> + /// <param name="samplerIndex">Type of the sampler pool indexing used for bound samplers</param> + public void SetSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex) + { + _samplerPoolGpuVa = gpuVa; + _samplerPoolMaximumId = maximumId; + _samplerIndex = samplerIndex; + _samplerPool = null; + } + + /// <summary> + /// Sets the current texture pool to be used. + /// </summary> + /// <param name="gpuVa">Start GPU virtual address of the pool</param> + /// <param name="maximumId">Maximum ID of the pool (total count minus one)</param> + public void SetTexturePool(ulong gpuVa, int maximumId) + { + _texturePoolGpuVa = gpuVa; + _texturePoolMaximumId = maximumId; + _texturePool = null; + } + + /// <summary> + /// Gets a texture and a sampler from their respective pools from a texture ID and a sampler ID. + /// </summary> + /// <param name="textureId">ID of the texture</param> + /// <param name="samplerId">ID of the sampler</param> + public (Texture, Sampler) GetTextureAndSampler(int textureId, int samplerId) + { + (TexturePool texturePool, SamplerPool samplerPool) = GetPools(); + + return (texturePool.Get(textureId), samplerPool.Get(samplerId)); + } + + /// <summary> + /// Updates the texture scale for a given texture or image. + /// </summary> + /// <param name="texture">Start GPU virtual address of the pool</param> + /// <param name="usageFlags">The related texture usage flags</param> + /// <param name="index">The texture/image binding index</param> + /// <param name="stage">The active shader stage</param> + /// <returns>True if the given texture has become blacklisted, indicating that its host texture may have changed.</returns> + private bool UpdateScale(Texture texture, TextureUsageFlags usageFlags, int index, ShaderStage stage) + { + float result = 1f; + bool changed = false; + + if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && texture != null) + { + if ((usageFlags & TextureUsageFlags.ResScaleUnsupported) != 0) + { + changed = texture.ScaleMode != TextureScaleMode.Blacklisted; + texture.BlacklistScale(); + } + else + { + switch (stage) + { + case ShaderStage.Fragment: + float scale = texture.ScaleFactor; + + if (scale != 1) + { + Texture activeTarget = _channel.TextureManager.GetAnyRenderTarget(); + + if (activeTarget != null && (activeTarget.Info.Width / (float)texture.Info.Width) == (activeTarget.Info.Height / (float)texture.Info.Height)) + { + // If the texture's size is a multiple of the sampler size, enable interpolation using gl_FragCoord. (helps "invent" new integer values between scaled pixels) + result = -scale; + break; + } + } + + result = scale; + break; + + case ShaderStage.Vertex: + int fragmentIndex = (int)ShaderStage.Fragment - 1; + index += _textureBindings[fragmentIndex].Length + _imageBindings[fragmentIndex].Length; + + result = texture.ScaleFactor; + break; + + case ShaderStage.Compute: + result = texture.ScaleFactor; + break; + } + } + } + + if (result != _scales[index]) + { + _scaleChanged = true; + + _scales[index] = result; + } + + return changed; + } + + /// <summary> + /// Determines if the vertex stage requires a scale value. + /// </summary> + private bool VertexRequiresScale() + { + for (int i = 0; i < _textureBindings[0].Length; i++) + { + if ((_textureBindings[0][i].Flags & TextureUsageFlags.NeedsScaleValue) != 0) + { + return true; + } + } + + for (int i = 0; i < _imageBindings[0].Length; i++) + { + if ((_imageBindings[0][i].Flags & TextureUsageFlags.NeedsScaleValue) != 0) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Uploads texture and image scales to the backend when they are used. + /// </summary> + private void CommitRenderScale() + { + // Stage 0 total: Compute or Vertex. + int total = _textureBindings[0].Length + _imageBindings[0].Length; + + int fragmentIndex = (int)ShaderStage.Fragment - 1; + int fragmentTotal = _isCompute ? 0 : (_textureBindings[fragmentIndex].Length + _imageBindings[fragmentIndex].Length); + + if (total != 0 && fragmentTotal != _lastFragmentTotal && VertexRequiresScale()) + { + // Must update scales in the support buffer if: + // - Vertex stage has bindings that require scale. + // - Fragment stage binding count has been updated since last render scale update. + + _scaleChanged = true; + } + + if (_scaleChanged) + { + if (!_isCompute) + { + total += fragmentTotal; // Add the fragment bindings to the total. + } + + _lastFragmentTotal = fragmentTotal; + + _context.Renderer.Pipeline.UpdateRenderScale(_scales, total, fragmentTotal); + + _scaleChanged = false; + } + } + + /// <summary> + /// Ensures that the bindings are visible to the host GPU. + /// Note: this actually performs the binding using the host graphics API. + /// </summary> + /// <param name="specState">Specialization state for the bound shader</param> + /// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns> + public bool CommitBindings(ShaderSpecializationState specState) + { + (TexturePool texturePool, SamplerPool samplerPool) = GetPools(); + + // Check if the texture pool has been modified since bindings were last committed. + // If it wasn't, then it's possible to avoid looking up textures again when the handle remains the same. + if (_cachedTexturePool != texturePool || _cachedSamplerPool != samplerPool) + { + Rebind(); + + _cachedTexturePool = texturePool; + _cachedSamplerPool = samplerPool; + } + + bool poolModified = false; + + if (texturePool != null) + { + int texturePoolSequence = texturePool.CheckModified(); + + if (_texturePoolSequence != texturePoolSequence) + { + poolModified = true; + _texturePoolSequence = texturePoolSequence; + } + } + + if (samplerPool != null) + { + int samplerPoolSequence = samplerPool.CheckModified(); + + if (_samplerPoolSequence != samplerPoolSequence) + { + poolModified = true; + _samplerPoolSequence = samplerPoolSequence; + } + } + + bool specStateMatches = true; + + if (_isCompute) + { + specStateMatches &= CommitTextureBindings(texturePool, samplerPool, ShaderStage.Compute, 0, poolModified, specState); + specStateMatches &= CommitImageBindings(texturePool, ShaderStage.Compute, 0, poolModified, specState); + } + else + { + for (ShaderStage stage = ShaderStage.Vertex; stage <= ShaderStage.Fragment; stage++) + { + int stageIndex = (int)stage - 1; + + specStateMatches &= CommitTextureBindings(texturePool, samplerPool, stage, stageIndex, poolModified, specState); + specStateMatches &= CommitImageBindings(texturePool, stage, stageIndex, poolModified, specState); + } + } + + CommitRenderScale(); + + return specStateMatches; + } + + /// <summary> + /// Fetch the constant buffers used for a texture to cache. + /// </summary> + /// <param name="stageIndex">Stage index of the constant buffer</param> + /// <param name="cachedTextureBufferIndex">The currently cached texture buffer index</param> + /// <param name="cachedSamplerBufferIndex">The currently cached sampler buffer index</param> + /// <param name="cachedTextureBuffer">The currently cached texture buffer data</param> + /// <param name="cachedSamplerBuffer">The currently cached sampler buffer data</param> + /// <param name="textureBufferIndex">The new texture buffer index</param> + /// <param name="samplerBufferIndex">The new sampler buffer index</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateCachedBuffer( + int stageIndex, + scoped ref int cachedTextureBufferIndex, + scoped ref int cachedSamplerBufferIndex, + scoped ref ReadOnlySpan<int> cachedTextureBuffer, + scoped ref ReadOnlySpan<int> cachedSamplerBuffer, + int textureBufferIndex, + int samplerBufferIndex) + { + if (textureBufferIndex != cachedTextureBufferIndex) + { + ref BufferBounds bounds = ref _channel.BufferManager.GetUniformBufferBounds(_isCompute, stageIndex, textureBufferIndex); + + cachedTextureBuffer = MemoryMarshal.Cast<byte, int>(_channel.MemoryManager.Physical.GetSpan(bounds.Address, (int)bounds.Size)); + cachedTextureBufferIndex = textureBufferIndex; + + if (samplerBufferIndex == textureBufferIndex) + { + cachedSamplerBuffer = cachedTextureBuffer; + cachedSamplerBufferIndex = samplerBufferIndex; + } + } + + if (samplerBufferIndex != cachedSamplerBufferIndex) + { + ref BufferBounds bounds = ref _channel.BufferManager.GetUniformBufferBounds(_isCompute, stageIndex, samplerBufferIndex); + + cachedSamplerBuffer = MemoryMarshal.Cast<byte, int>(_channel.MemoryManager.Physical.GetSpan(bounds.Address, (int)bounds.Size)); + cachedSamplerBufferIndex = samplerBufferIndex; + } + } + + /// <summary> + /// Counts the total number of texture bindings used by all shader stages. + /// </summary> + /// <returns>The total amount of textures used</returns> + private int GetTextureBindingsCount() + { + int count = 0; + + for (int i = 0; i < _textureBindings.Length; i++) + { + if (_textureBindings[i] != null) + { + count += _textureBindings[i].Length; + } + } + + return count; + } + + /// <summary> + /// Ensures that the texture bindings are visible to the host GPU. + /// Note: this actually performs the binding using the host graphics API. + /// </summary> + /// <param name="texturePool">The current texture pool</param> + /// <param name="samplerPool">The current sampler pool</param> + /// <param name="stage">The shader stage using the textures to be bound</param> + /// <param name="stageIndex">The stage number of the specified shader stage</param + /// <param name="poolModified">True if either the texture or sampler pool was modified, false otherwise</param> + /// <param name="specState">Specialization state for the bound shader</param> + /// <returns>True if all bound textures match the current shader specialiation state, false otherwise</returns> + private bool CommitTextureBindings( + TexturePool texturePool, + SamplerPool samplerPool, + ShaderStage stage, + int stageIndex, + bool poolModified, + ShaderSpecializationState specState) + { + int textureCount = _textureBindings[stageIndex].Length; + if (textureCount == 0) + { + return true; + } + + if (texturePool == null) + { + Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses textures, but texture pool was not set."); + return true; + } + + bool specStateMatches = true; + + int cachedTextureBufferIndex = -1; + int cachedSamplerBufferIndex = -1; + ReadOnlySpan<int> cachedTextureBuffer = Span<int>.Empty; + ReadOnlySpan<int> cachedSamplerBuffer = Span<int>.Empty; + + for (int index = 0; index < textureCount; index++) + { + TextureBindingInfo bindingInfo = _textureBindings[stageIndex][index]; + TextureUsageFlags usageFlags = bindingInfo.Flags; + + (int textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, _textureBufferIndex); + + UpdateCachedBuffer(stageIndex, ref cachedTextureBufferIndex, ref cachedSamplerBufferIndex, ref cachedTextureBuffer, ref cachedSamplerBuffer, textureBufferIndex, samplerBufferIndex); + + int packedId = TextureHandle.ReadPackedId(bindingInfo.Handle, cachedTextureBuffer, cachedSamplerBuffer); + int textureId = TextureHandle.UnpackTextureId(packedId); + int samplerId; + + if (_samplerIndex == SamplerIndex.ViaHeaderIndex) + { + samplerId = textureId; + } + else + { + samplerId = TextureHandle.UnpackSamplerId(packedId); + } + + ref TextureState state = ref _textureState[bindingInfo.Binding]; + + if (!poolModified && + state.TextureHandle == textureId && + state.SamplerHandle == samplerId && + state.CachedTexture != null && + state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence && + state.CachedSampler?.IsDisposed != true) + { + // The texture is already bound. + state.CachedTexture.SynchronizeMemory(); + + if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && + UpdateScale(state.CachedTexture, usageFlags, index, stage)) + { + ITexture hostTextureRebind = state.CachedTexture.GetTargetTexture(bindingInfo.Target); + + state.Texture = hostTextureRebind; + + _context.Renderer.Pipeline.SetTextureAndSampler(stage, bindingInfo.Binding, hostTextureRebind, state.Sampler); + } + + continue; + } + + state.TextureHandle = textureId; + state.SamplerHandle = samplerId; + + ref readonly TextureDescriptor descriptor = ref texturePool.GetForBinding(textureId, out Texture texture); + + specStateMatches &= specState.MatchesTexture(stage, index, descriptor); + + Sampler sampler = samplerPool?.Get(samplerId); + + ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + ISampler hostSampler = sampler?.GetHostSampler(texture); + + if (hostTexture != null && texture.Target == Target.TextureBuffer) + { + // Ensure that the buffer texture is using the correct buffer as storage. + // Buffers are frequently re-created to accomodate larger data, so we need to re-bind + // to ensure we're not using a old buffer that was already deleted. + _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, bindingInfo.Format, false); + + // Cache is not used for buffer texture, it must always rebind. + state.CachedTexture = null; + } + else + { + bool textureOrSamplerChanged = state.Texture != hostTexture || state.Sampler != hostSampler; + + if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && + UpdateScale(texture, usageFlags, index, stage)) + { + hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + textureOrSamplerChanged = true; + } + + if (textureOrSamplerChanged) + { + state.Texture = hostTexture; + state.Sampler = hostSampler; + + _context.Renderer.Pipeline.SetTextureAndSampler(stage, bindingInfo.Binding, hostTexture, hostSampler); + } + + state.CachedTexture = texture; + state.CachedSampler = sampler; + state.InvalidatedSequence = texture?.InvalidatedSequence ?? 0; + } + } + + return specStateMatches; + } + + /// <summary> + /// Ensures that the image bindings are visible to the host GPU. + /// Note: this actually performs the binding using the host graphics API. + /// </summary> + /// <param name="pool">The current texture pool</param> + /// <param name="stage">The shader stage using the textures to be bound</param> + /// <param name="stageIndex">The stage number of the specified shader stage</param> + /// <param name="poolModified">True if either the texture or sampler pool was modified, false otherwise</param> + /// <param name="specState">Specialization state for the bound shader</param> + /// <returns>True if all bound images match the current shader specialiation state, false otherwise</returns> + private bool CommitImageBindings(TexturePool pool, ShaderStage stage, int stageIndex, bool poolModified, ShaderSpecializationState specState) + { + int imageCount = _imageBindings[stageIndex].Length; + if (imageCount == 0) + { + return true; + } + + if (pool == null) + { + Logger.Error?.Print(LogClass.Gpu, $"Shader stage \"{stage}\" uses images, but texture pool was not set."); + return true; + } + + // Scales for images appear after the texture ones. + int baseScaleIndex = _textureBindings[stageIndex].Length; + + int cachedTextureBufferIndex = -1; + int cachedSamplerBufferIndex = -1; + ReadOnlySpan<int> cachedTextureBuffer = Span<int>.Empty; + ReadOnlySpan<int> cachedSamplerBuffer = Span<int>.Empty; + + bool specStateMatches = true; + + for (int index = 0; index < imageCount; index++) + { + TextureBindingInfo bindingInfo = _imageBindings[stageIndex][index]; + TextureUsageFlags usageFlags = bindingInfo.Flags; + int scaleIndex = baseScaleIndex + index; + + (int textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, _textureBufferIndex); + + UpdateCachedBuffer(stageIndex, ref cachedTextureBufferIndex, ref cachedSamplerBufferIndex, ref cachedTextureBuffer, ref cachedSamplerBuffer, textureBufferIndex, samplerBufferIndex); + + int packedId = TextureHandle.ReadPackedId(bindingInfo.Handle, cachedTextureBuffer, cachedSamplerBuffer); + int textureId = TextureHandle.UnpackTextureId(packedId); + + ref TextureState state = ref _imageState[bindingInfo.Binding]; + + bool isStore = bindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore); + + if (!poolModified && + state.TextureHandle == textureId && + state.CachedTexture != null && + state.CachedTexture.InvalidatedSequence == state.InvalidatedSequence) + { + Texture cachedTexture = state.CachedTexture; + + // The texture is already bound. + cachedTexture.SynchronizeMemory(); + + if (isStore) + { + cachedTexture?.SignalModified(); + } + + Format format = bindingInfo.Format == 0 ? cachedTexture.Format : bindingInfo.Format; + + if (state.ImageFormat != format || + ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && + UpdateScale(state.CachedTexture, usageFlags, scaleIndex, stage))) + { + ITexture hostTextureRebind = state.CachedTexture.GetTargetTexture(bindingInfo.Target); + + state.Texture = hostTextureRebind; + state.ImageFormat = format; + + _context.Renderer.Pipeline.SetImage(bindingInfo.Binding, hostTextureRebind, format); + } + + continue; + } + + state.TextureHandle = textureId; + + ref readonly TextureDescriptor descriptor = ref pool.GetForBinding(textureId, out Texture texture); + + specStateMatches &= specState.MatchesImage(stage, index, descriptor); + + ITexture hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + + if (hostTexture != null && texture.Target == Target.TextureBuffer) + { + // Ensure that the buffer texture is using the correct buffer as storage. + // Buffers are frequently re-created to accomodate larger data, so we need to re-bind + // to ensure we're not using a old buffer that was already deleted. + + Format format = bindingInfo.Format; + + if (format == 0 && texture != null) + { + format = texture.Format; + } + + _channel.BufferManager.SetBufferTextureStorage(stage, hostTexture, texture.Range.GetSubRange(0).Address, texture.Size, bindingInfo, format, true); + + // Cache is not used for buffer texture, it must always rebind. + state.CachedTexture = null; + } + else + { + if (isStore) + { + texture?.SignalModified(); + } + + if ((usageFlags & TextureUsageFlags.NeedsScaleValue) != 0 && + UpdateScale(texture, usageFlags, scaleIndex, stage)) + { + hostTexture = texture?.GetTargetTexture(bindingInfo.Target); + } + + if (state.Texture != hostTexture) + { + state.Texture = hostTexture; + + Format format = bindingInfo.Format; + + if (format == 0 && texture != null) + { + format = texture.Format; + } + + state.ImageFormat = format; + + _context.Renderer.Pipeline.SetImage(bindingInfo.Binding, hostTexture, format); + } + + state.CachedTexture = texture; + state.InvalidatedSequence = texture?.InvalidatedSequence ?? 0; + } + } + + return specStateMatches; + } + + /// <summary> + /// Gets the texture descriptor for a given texture handle. + /// </summary> + /// <param name="poolGpuVa">GPU virtual address of the texture pool</param> + /// <param name="bufferIndex">Index of the constant buffer with texture handles</param> + /// <param name="maximumId">Maximum ID of the texture pool</param> + /// <param name="stageIndex">The stage number where the texture is bound</param> + /// <param name="handle">The texture handle</param> + /// <param name="cbufSlot">The texture handle's constant buffer slot</param> + /// <returns>The texture descriptor for the specified texture</returns> + public TextureDescriptor GetTextureDescriptor( + ulong poolGpuVa, + int bufferIndex, + int maximumId, + int stageIndex, + int handle, + int cbufSlot) + { + (int textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(cbufSlot, bufferIndex); + + int packedId = ReadPackedId(stageIndex, handle, textureBufferIndex, samplerBufferIndex); + int textureId = TextureHandle.UnpackTextureId(packedId); + + ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa); + + TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId); + + TextureDescriptor descriptor; + + if (texturePool.IsValidId(textureId)) + { + descriptor = texturePool.GetDescriptor(textureId); + } + else + { + // If the ID is not valid, we just return a default descriptor with the most common state. + // Since this is used for shader specialization, doing so might avoid the need for recompilations. + descriptor = new TextureDescriptor(); + descriptor.Word4 |= (uint)TextureTarget.Texture2D << 23; + descriptor.Word5 |= 1u << 31; // Coords normalized. + } + + return descriptor; + } + + /// <summary> + /// Reads a packed texture and sampler ID (basically, the real texture handle) + /// from the texture constant buffer. + /// </summary> + /// <param name="stageIndex">The number of the shader stage where the texture is bound</param> + /// <param name="wordOffset">A word offset of the handle on the buffer (the "fake" shader handle)</param> + /// <param name="textureBufferIndex">Index of the constant buffer holding the texture handles</param> + /// <param name="samplerBufferIndex">Index of the constant buffer holding the sampler handles</param> + /// <returns>The packed texture and sampler ID (the real texture handle)</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadPackedId(int stageIndex, int wordOffset, int textureBufferIndex, int samplerBufferIndex) + { + (int textureWordOffset, int samplerWordOffset, TextureHandleType handleType) = TextureHandle.UnpackOffsets(wordOffset); + + ulong textureBufferAddress = _isCompute + ? _channel.BufferManager.GetComputeUniformBufferAddress(textureBufferIndex) + : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, textureBufferIndex); + + int handle = textureBufferAddress != 0 + ? _channel.MemoryManager.Physical.Read<int>(textureBufferAddress + (uint)textureWordOffset * 4) + : 0; + + // The "wordOffset" (which is really the immediate value used on texture instructions on the shader) + // is a 13-bit value. However, in order to also support separate samplers and textures (which uses + // bindless textures on the shader), we extend it with another value on the higher 16 bits with + // another offset for the sampler. + // The shader translator has code to detect separate texture and sampler uses with a bindless texture, + // turn that into a regular texture access and produce those special handles with values on the higher 16 bits. + if (handleType != TextureHandleType.CombinedSampler) + { + int samplerHandle; + + if (handleType != TextureHandleType.SeparateConstantSamplerHandle) + { + ulong samplerBufferAddress = _isCompute + ? _channel.BufferManager.GetComputeUniformBufferAddress(samplerBufferIndex) + : _channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, samplerBufferIndex); + + samplerHandle = samplerBufferAddress != 0 + ? _channel.MemoryManager.Physical.Read<int>(samplerBufferAddress + (uint)samplerWordOffset * 4) + : 0; + } + else + { + samplerHandle = samplerWordOffset; + } + + if (handleType == TextureHandleType.SeparateSamplerId || + handleType == TextureHandleType.SeparateConstantSamplerHandle) + { + samplerHandle <<= 20; + } + + handle |= samplerHandle; + } + + return handle; + } + + /// <summary> + /// Gets the texture and sampler pool for the GPU virtual address that are currently set. + /// </summary> + /// <returns>The texture and sampler pools</returns> + private (TexturePool, SamplerPool) GetPools() + { + MemoryManager memoryManager = _channel.MemoryManager; + + TexturePool texturePool = _texturePool; + SamplerPool samplerPool = _samplerPool; + + if (texturePool == null) + { + ulong poolAddress = memoryManager.Translate(_texturePoolGpuVa); + + if (poolAddress != MemoryManager.PteUnmapped) + { + texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, _texturePoolMaximumId); + _texturePool = texturePool; + } + } + + if (samplerPool == null) + { + ulong poolAddress = memoryManager.Translate(_samplerPoolGpuVa); + + if (poolAddress != MemoryManager.PteUnmapped) + { + samplerPool = _samplerPoolCache.FindOrCreate(_channel, poolAddress, _samplerPoolMaximumId); + _samplerPool = samplerPool; + } + } + + return (texturePool, samplerPool); + } + + /// <summary> + /// Forces the texture and sampler pools to be re-loaded from the cache on next use. + /// </summary> + /// <remarks> + /// This should be called if the memory mappings change, to ensure the correct pools are being used. + /// </remarks> + public void ReloadPools() + { + _samplerPool = null; + _texturePool = null; + } + + /// <summary> + /// Force all bound textures and images to be rebound the next time CommitBindings is called. + /// </summary> + public void Rebind() + { + Array.Clear(_textureState); + Array.Clear(_imageState); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs new file mode 100644 index 00000000..c3243cf2 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs @@ -0,0 +1,1180 @@ +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Engine.Threed; +using Ryujinx.Graphics.Gpu.Engine.Twod; +using Ryujinx.Graphics.Gpu.Engine.Types; +using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Texture; +using Ryujinx.Memory.Range; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture cache. + /// </summary> + class TextureCache : IDisposable + { + private readonly struct OverlapInfo + { + public TextureViewCompatibility Compatibility { get; } + public int FirstLayer { get; } + public int FirstLevel { get; } + + public OverlapInfo(TextureViewCompatibility compatibility, int firstLayer, int firstLevel) + { + Compatibility = compatibility; + FirstLayer = firstLayer; + FirstLevel = firstLevel; + } + } + + private const int OverlapsBufferInitialCapacity = 10; + private const int OverlapsBufferMaxCapacity = 10000; + + private readonly GpuContext _context; + private readonly PhysicalMemory _physicalMemory; + + private readonly MultiRangeList<Texture> _textures; + private readonly HashSet<Texture> _partiallyMappedTextures; + + private Texture[] _textureOverlaps; + private OverlapInfo[] _overlapInfo; + + private readonly AutoDeleteCache _cache; + + /// <summary> + /// Constructs a new instance of the texture manager. + /// </summary> + /// <param name="context">The GPU context that the texture manager belongs to</param> + /// <param name="physicalMemory">Physical memory where the textures managed by this cache are mapped</param> + public TextureCache(GpuContext context, PhysicalMemory physicalMemory) + { + _context = context; + _physicalMemory = physicalMemory; + + _textures = new MultiRangeList<Texture>(); + _partiallyMappedTextures = new HashSet<Texture>(); + + _textureOverlaps = new Texture[OverlapsBufferInitialCapacity]; + _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity]; + + _cache = new AutoDeleteCache(); + } + + /// <summary> + /// Handles removal of textures written to a memory region being unmapped. + /// </summary> + /// <param name="sender">Sender object</param> + /// <param name="e">Event arguments</param> + public void MemoryUnmappedHandler(object sender, UnmapEventArgs e) + { + Texture[] overlaps = new Texture[10]; + int overlapCount; + + MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size); + + lock (_textures) + { + overlapCount = _textures.FindOverlaps(unmapped, ref overlaps); + } + + for (int i = 0; i < overlapCount; i++) + { + overlaps[i].Unmapped(unmapped); + } + + // If any range was previously unmapped, we also need to purge + // all partially mapped texture, as they might be fully mapped now. + for (int i = 0; i < unmapped.Count; i++) + { + if (unmapped.GetSubRange(i).Address == MemoryManager.PteUnmapped) + { + lock (_partiallyMappedTextures) + { + foreach (var texture in _partiallyMappedTextures) + { + texture.Unmapped(unmapped); + } + } + + break; + } + } + } + + /// <summary> + /// Determines if a given texture is eligible for upscaling from its info. + /// </summary> + /// <param name="info">The texture info to check</param> + /// <param name="withUpscale">True if the user of the texture would prefer it to be upscaled immediately</param> + /// <returns>True if eligible</returns> + private static TextureScaleMode IsUpscaleCompatible(TextureInfo info, bool withUpscale) + { + if ((info.Target == Target.Texture2D || info.Target == Target.Texture2DArray || info.Target == Target.Texture2DMultisample) && !info.FormatInfo.IsCompressed) + { + return UpscaleSafeMode(info) ? (withUpscale ? TextureScaleMode.Scaled : TextureScaleMode.Eligible) : TextureScaleMode.Undesired; + } + + return TextureScaleMode.Blacklisted; + } + + /// <summary> + /// Determines if a given texture is "safe" for upscaling from its info. + /// Note that this is different from being compatible - this elilinates targets that would have detrimental effects when scaled. + /// </summary> + /// <param name="info">The texture info to check</param> + /// <returns>True if safe</returns> + private static bool UpscaleSafeMode(TextureInfo info) + { + // While upscaling works for all targets defined by IsUpscaleCompatible, we additionally blacklist targets here that + // may have undesirable results (upscaling blur textures) or simply waste GPU resources (upscaling texture atlas). + + if (info.Levels > 3) + { + // Textures with more than 3 levels are likely to be game textures, rather than render textures. + // Small textures with full mips are likely to be removed by the next check. + return false; + } + + if (info.Width < 8 || info.Height < 8) + { + // Discount textures with small dimensions. + return false; + } + + int widthAlignment = (info.IsLinear ? Constants.StrideAlignment : Constants.GobAlignment) / info.FormatInfo.BytesPerPixel; + + if (!(info.FormatInfo.Format.IsDepthOrStencil() || info.FormatInfo.Components == 1)) + { + // Discount square textures that aren't depth-stencil like. (excludes game textures, cubemap faces, most 3D texture LUT, texture atlas) + // Detect if the texture is possibly square. Widths may be aligned, so to remove the uncertainty we align both the width and height. + + bool possiblySquare = BitUtils.AlignUp(info.Width, widthAlignment) == BitUtils.AlignUp(info.Height, widthAlignment); + + if (possiblySquare) + { + return false; + } + } + + if (info.Height < 360) + { + int aspectWidth = (int)MathF.Ceiling((info.Height / 9f) * 16f); + int aspectMaxWidth = BitUtils.AlignUp(aspectWidth, widthAlignment); + int aspectMinWidth = BitUtils.AlignDown(aspectWidth, widthAlignment); + + if (info.Width >= aspectMinWidth && info.Width <= aspectMaxWidth && info.Height < 360) + { + // Targets that are roughly 16:9 can only be rescaled if they're equal to or above 360p. (excludes blur and bloom textures) + return false; + } + } + + if (info.Width == info.Height * info.Height) + { + // Possibly used for a "3D texture" drawn onto a 2D surface. + // Some games do this to generate a tone mapping LUT without rendering into 3D texture slices. + + return false; + } + + return true; + } + + /// <summary> + /// Lifts the texture to the top of the AutoDeleteCache. This is primarily used to enforce that + /// data written to a target will be flushed to memory should the texture be deleted, but also + /// keeps rendered textures alive without a pool reference. + /// </summary> + /// <param name="texture">Texture to lift</param> + public void Lift(Texture texture) + { + _cache.Lift(texture); + } + + /// <summary> + /// Attempts to update a texture's physical memory range. + /// Returns false if there is an existing texture that matches with the updated range. + /// </summary> + /// <param name="texture">Texture to update</param> + /// <param name="range">New physical memory range</param> + /// <returns>True if the mapping was updated, false otherwise</returns> + public bool UpdateMapping(Texture texture, MultiRange range) + { + // There cannot be an existing texture compatible with this mapping in the texture cache already. + int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps); + + for (int i = 0; i < overlapCount; i++) + { + var other = _textureOverlaps[i]; + + if (texture != other && + (texture.IsViewCompatible(other.Info, other.Range, true, other.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible || + other.IsViewCompatible(texture.Info, texture.Range, true, texture.LayerSize, _context.Capabilities, out _, out _) != TextureViewCompatibility.Incompatible)) + { + return false; + } + } + + _textures.Remove(texture); + + texture.ReplaceRange(range); + + _textures.Add(texture); + + return true; + } + + /// <summary> + /// Tries to find an existing texture, or create a new one if not found. + /// </summary> + /// <param name="memoryManager">GPU memory manager where the texture is mapped</param> + /// <param name="copyTexture">Copy texture to find or create</param> + /// <param name="offset">Offset to be added to the physical texture address</param> + /// <param name="formatInfo">Format information of the copy texture</param> + /// <param name="preferScaling">Indicates if the texture should be scaled from the start</param> + /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param> + /// <returns>The texture</returns> + public Texture FindOrCreateTexture( + MemoryManager memoryManager, + TwodTexture copyTexture, + ulong offset, + FormatInfo formatInfo, + bool shouldCreate, + bool preferScaling, + Size sizeHint) + { + int gobBlocksInY = copyTexture.MemoryLayout.UnpackGobBlocksInY(); + int gobBlocksInZ = copyTexture.MemoryLayout.UnpackGobBlocksInZ(); + + int width; + + if (copyTexture.LinearLayout) + { + width = copyTexture.Stride / formatInfo.BytesPerPixel; + } + else + { + width = copyTexture.Width; + } + + TextureInfo info = new TextureInfo( + copyTexture.Address.Pack() + offset, + GetMinimumWidthInGob(width, sizeHint.Width, formatInfo.BytesPerPixel, copyTexture.LinearLayout), + copyTexture.Height, + copyTexture.Depth, + 1, + 1, + 1, + copyTexture.Stride, + copyTexture.LinearLayout, + gobBlocksInY, + gobBlocksInZ, + 1, + Target.Texture2D, + formatInfo); + + TextureSearchFlags flags = TextureSearchFlags.ForCopy; + + if (preferScaling) + { + flags |= TextureSearchFlags.WithUpscale; + } + + if (!shouldCreate) + { + flags |= TextureSearchFlags.NoCreate; + } + + Texture texture = FindOrCreateTexture(memoryManager, flags, info, 0); + + texture?.SynchronizeMemory(); + + return texture; + } + + /// <summary> + /// Tries to find an existing texture, or create a new one if not found. + /// </summary> + /// <param name="memoryManager">GPU memory manager where the texture is mapped</param> + /// <param name="colorState">Color buffer texture to find or create</param> + /// <param name="layered">Indicates if the texture might be accessed with a non-zero layer index</param> + /// <param name="samplesInX">Number of samples in the X direction, for MSAA</param> + /// <param name="samplesInY">Number of samples in the Y direction, for MSAA</param> + /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param> + /// <returns>The texture</returns> + public Texture FindOrCreateTexture( + MemoryManager memoryManager, + RtColorState colorState, + bool layered, + int samplesInX, + int samplesInY, + Size sizeHint) + { + bool isLinear = colorState.MemoryLayout.UnpackIsLinear(); + + int gobBlocksInY = colorState.MemoryLayout.UnpackGobBlocksInY(); + int gobBlocksInZ = colorState.MemoryLayout.UnpackGobBlocksInZ(); + + Target target; + + if (colorState.MemoryLayout.UnpackIsTarget3D()) + { + target = Target.Texture3D; + } + else if ((samplesInX | samplesInY) != 1) + { + target = colorState.Depth > 1 && layered + ? Target.Texture2DMultisampleArray + : Target.Texture2DMultisample; + } + else + { + target = colorState.Depth > 1 && layered + ? Target.Texture2DArray + : Target.Texture2D; + } + + FormatInfo formatInfo = colorState.Format.Convert(); + + int width, stride; + + // For linear textures, the width value is actually the stride. + // We can easily get the width by dividing the stride by the bpp, + // since the stride is the total number of bytes occupied by a + // line. The stride should also meet alignment constraints however, + // so the width we get here is the aligned width. + if (isLinear) + { + width = colorState.WidthOrStride / formatInfo.BytesPerPixel; + stride = colorState.WidthOrStride; + } + else + { + width = colorState.WidthOrStride; + stride = 0; + } + + TextureInfo info = new TextureInfo( + colorState.Address.Pack(), + GetMinimumWidthInGob(width, sizeHint.Width, formatInfo.BytesPerPixel, isLinear), + colorState.Height, + colorState.Depth, + 1, + samplesInX, + samplesInY, + stride, + isLinear, + gobBlocksInY, + gobBlocksInZ, + 1, + target, + formatInfo); + + int layerSize = !isLinear ? colorState.LayerSize * 4 : 0; + + Texture texture = FindOrCreateTexture(memoryManager, TextureSearchFlags.WithUpscale, info, layerSize); + + texture?.SynchronizeMemory(); + + return texture; + } + + /// <summary> + /// Tries to find an existing texture, or create a new one if not found. + /// </summary> + /// <param name="memoryManager">GPU memory manager where the texture is mapped</param> + /// <param name="dsState">Depth-stencil buffer texture to find or create</param> + /// <param name="size">Size of the depth-stencil texture</param> + /// <param name="layered">Indicates if the texture might be accessed with a non-zero layer index</param> + /// <param name="samplesInX">Number of samples in the X direction, for MSAA</param> + /// <param name="samplesInY">Number of samples in the Y direction, for MSAA</param> + /// <param name="sizeHint">A hint indicating the minimum used size for the texture</param> + /// <returns>The texture</returns> + public Texture FindOrCreateTexture( + MemoryManager memoryManager, + RtDepthStencilState dsState, + Size3D size, + bool layered, + int samplesInX, + int samplesInY, + Size sizeHint) + { + int gobBlocksInY = dsState.MemoryLayout.UnpackGobBlocksInY(); + int gobBlocksInZ = dsState.MemoryLayout.UnpackGobBlocksInZ(); + + Target target; + + if (dsState.MemoryLayout.UnpackIsTarget3D()) + { + target = Target.Texture3D; + } + else if ((samplesInX | samplesInY) != 1) + { + target = size.Depth > 1 && layered + ? Target.Texture2DMultisampleArray + : Target.Texture2DMultisample; + } + else + { + target = size.Depth > 1 && layered + ? Target.Texture2DArray + : Target.Texture2D; + } + + FormatInfo formatInfo = dsState.Format.Convert(); + + TextureInfo info = new TextureInfo( + dsState.Address.Pack(), + GetMinimumWidthInGob(size.Width, sizeHint.Width, formatInfo.BytesPerPixel, false), + size.Height, + size.Depth, + 1, + samplesInX, + samplesInY, + 0, + false, + gobBlocksInY, + gobBlocksInZ, + 1, + target, + formatInfo); + + Texture texture = FindOrCreateTexture(memoryManager, TextureSearchFlags.WithUpscale, info, dsState.LayerSize * 4); + + texture?.SynchronizeMemory(); + + return texture; + } + + /// <summary> + /// For block linear textures, gets the minimum width of the texture + /// that would still have the same number of GOBs per row as the original width. + /// </summary> + /// <param name="width">The possibly aligned texture width</param> + /// <param name="minimumWidth">The minimum width that the texture may have without losing data</param> + /// <param name="bytesPerPixel">Bytes per pixel of the texture format</param> + /// <param name="isLinear">True if the texture is linear, false for block linear</param> + /// <returns>The minimum width of the texture with the same amount of GOBs per row</returns> + private static int GetMinimumWidthInGob(int width, int minimumWidth, int bytesPerPixel, bool isLinear) + { + if (isLinear || (uint)minimumWidth >= (uint)width) + { + return width; + } + + // Calculate the minimum possible that would not cause data loss + // and would be still within the same GOB (aligned size would be the same). + // This is useful for render and copy operations, where we don't know the + // exact width of the texture, but it doesn't matter, as long the texture is + // at least as large as the region being rendered or copied. + + int alignment = 64 / bytesPerPixel; + int widthAligned = BitUtils.AlignUp(width, alignment); + + return Math.Clamp(widthAligned - alignment + 1, minimumWidth, widthAligned); + } + + /// <summary> + /// Tries to find an existing texture, or create a new one if not found. + /// </summary> + /// <param name="memoryManager">GPU memory manager where the texture is mapped</param> + /// <param name="flags">The texture search flags, defines texture comparison rules</param> + /// <param name="info">Texture information of the texture to be found or created</param> + /// <param name="layerSize">Size in bytes of a single texture layer</param> + /// <param name="range">Optional ranges of physical memory where the texture data is located</param> + /// <returns>The texture</returns> + public Texture FindOrCreateTexture( + MemoryManager memoryManager, + TextureSearchFlags flags, + TextureInfo info, + int layerSize = 0, + MultiRange? range = null) + { + bool isSamplerTexture = (flags & TextureSearchFlags.ForSampler) != 0; + + TextureScaleMode scaleMode = IsUpscaleCompatible(info, (flags & TextureSearchFlags.WithUpscale) != 0); + + ulong address; + + if (range != null) + { + address = range.Value.GetSubRange(0).Address; + } + else + { + address = memoryManager.Translate(info.GpuAddress); + + // If the start address is unmapped, let's try to find a page of memory that is mapped. + if (address == MemoryManager.PteUnmapped) + { + // Make sure that the dimensions are valid before calculating the texture size. + if (info.Width < 1 || info.Height < 1 || info.Levels < 1) + { + return null; + } + + if ((info.Target == Target.Texture3D || + info.Target == Target.Texture2DArray || + info.Target == Target.Texture2DMultisampleArray || + info.Target == Target.CubemapArray) && info.DepthOrLayers < 1) + { + return null; + } + + ulong dataSize = (ulong)info.CalculateSizeInfo(layerSize).TotalSize; + + address = memoryManager.TranslateFirstMapped(info.GpuAddress, dataSize); + } + + // If address is still invalid, the texture is fully unmapped, so it has no data, just return null. + if (address == MemoryManager.PteUnmapped) + { + return null; + } + } + + int sameAddressOverlapsCount; + + lock (_textures) + { + // Try to find a perfect texture match, with the same address and parameters. + sameAddressOverlapsCount = _textures.FindOverlaps(address, ref _textureOverlaps); + } + + Texture texture = null; + + TextureMatchQuality bestQuality = TextureMatchQuality.NoMatch; + + for (int index = 0; index < sameAddressOverlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + + TextureMatchQuality matchQuality = overlap.IsExactMatch(info, flags); + + if (matchQuality != TextureMatchQuality.NoMatch) + { + // If the parameters match, we need to make sure the texture is mapped to the same memory regions. + if (range != null) + { + // If a range of memory was supplied, just check if the ranges match. + if (!overlap.Range.Equals(range.Value)) + { + continue; + } + } + else + { + // If no range was supplied, we can check if the GPU virtual address match. If they do, + // we know the textures are located at the same memory region. + // If they don't, it may still be mapped to the same physical region, so we + // do a more expensive check to tell if they are mapped into the same physical regions. + // If the GPU VA for the texture has ever been unmapped, then the range must be checked regardless. + if ((overlap.Info.GpuAddress != info.GpuAddress || overlap.ChangedMapping) && + !memoryManager.CompareRange(overlap.Range, info.GpuAddress)) + { + continue; + } + } + } + + if (matchQuality == TextureMatchQuality.Perfect) + { + texture = overlap; + break; + } + else if (matchQuality > bestQuality) + { + texture = overlap; + bestQuality = matchQuality; + } + } + + if (texture != null) + { + texture.SynchronizeMemory(); + + return texture; + } + else if (flags.HasFlag(TextureSearchFlags.NoCreate)) + { + return null; + } + + // Calculate texture sizes, used to find all overlapping textures. + SizeInfo sizeInfo = info.CalculateSizeInfo(layerSize); + + ulong size = (ulong)sizeInfo.TotalSize; + bool partiallyMapped = false; + + if (range == null) + { + range = memoryManager.GetPhysicalRegions(info.GpuAddress, size); + + for (int i = 0; i < range.Value.Count; i++) + { + if (range.Value.GetSubRange(i).Address == MemoryManager.PteUnmapped) + { + partiallyMapped = true; + break; + } + } + } + + // Find view compatible matches. + int overlapsCount; + + lock (_textures) + { + overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps); + } + + if (_overlapInfo.Length != _textureOverlaps.Length) + { + Array.Resize(ref _overlapInfo, _textureOverlaps.Length); + } + + // =============== Find Texture View of Existing Texture =============== + + int fullyCompatible = 0; + + // Evaluate compatibility of overlaps, add temporary references + + for (int index = 0; index < overlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible( + info, + range.Value, + isSamplerTexture, + sizeInfo.LayerSize, + _context.Capabilities, + out int firstLayer, + out int firstLevel); + + if (overlapCompatibility == TextureViewCompatibility.Full) + { + if (overlap.IsView) + { + overlapCompatibility = TextureViewCompatibility.CopyOnly; + } + else + { + fullyCompatible++; + } + } + + _overlapInfo[index] = new OverlapInfo(overlapCompatibility, firstLayer, firstLevel); + overlap.IncrementReferenceCount(); + } + + // Search through the overlaps to find a compatible view and establish any copy dependencies. + + for (int index = 0; index < overlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; + + if (oInfo.Compatibility == TextureViewCompatibility.Full) + { + if (!isSamplerTexture) + { + // If this is not a sampler texture, the size might be different from the requested size, + // so we need to make sure the texture information has the correct size for this base texture, + // before creating the view. + info = info.CreateInfoForLevelView(overlap, oInfo.FirstLevel); + } + + texture = overlap.CreateView(info, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel); + texture.SynchronizeMemory(); + break; + } + else if (oInfo.Compatibility == TextureViewCompatibility.CopyOnly && fullyCompatible == 0) + { + // Only copy compatible. If there's another choice for a FULLY compatible texture, choose that instead. + + texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode); + + texture.InitializeGroup(true, true, new List<TextureIncompatibleOverlap>()); + texture.InitializeData(false, false); + + overlap.SynchronizeMemory(); + overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true); + break; + } + } + + if (texture != null) + { + // This texture could be a view of multiple parent textures with different storages, even if it is a view. + // When a texture is created, make sure all possible dependencies to other textures are created as copies. + // (even if it could be fulfilled without a copy) + + for (int index = 0; index < overlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; + + if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible) + { + if (!overlap.IsView && texture.DataOverlaps(overlap, oInfo.Compatibility)) + { + texture.Group.RegisterIncompatibleOverlap(new TextureIncompatibleOverlap(overlap.Group, oInfo.Compatibility), true); + } + } + else if (overlap.Group != texture.Group) + { + overlap.SynchronizeMemory(); + overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true); + } + } + + texture.SynchronizeMemory(); + } + + // =============== Create a New Texture =============== + + // No match, create a new texture. + if (texture == null) + { + texture = new Texture(_context, _physicalMemory, info, sizeInfo, range.Value, scaleMode); + + // Step 1: Find textures that are view compatible with the new texture. + // Any textures that are incompatible will contain garbage data, so they should be removed where possible. + + int viewCompatible = 0; + fullyCompatible = 0; + bool setData = isSamplerTexture || overlapsCount == 0 || flags.HasFlag(TextureSearchFlags.ForCopy); + + bool hasLayerViews = false; + bool hasMipViews = false; + + var incompatibleOverlaps = new List<TextureIncompatibleOverlap>(); + + for (int index = 0; index < overlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + bool overlapInCache = overlap.CacheNode != null; + + TextureViewCompatibility compatibility = texture.IsViewCompatible( + overlap.Info, + overlap.Range, + exactSize: true, + overlap.LayerSize, + _context.Capabilities, + out int firstLayer, + out int firstLevel); + + if (overlap.IsView && compatibility == TextureViewCompatibility.Full) + { + compatibility = TextureViewCompatibility.CopyOnly; + } + + if (compatibility > TextureViewCompatibility.LayoutIncompatible) + { + _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); + _textureOverlaps[index] = _textureOverlaps[viewCompatible]; + _textureOverlaps[viewCompatible] = overlap; + + if (compatibility == TextureViewCompatibility.Full) + { + if (viewCompatible != fullyCompatible) + { + // Swap overlaps so that the fully compatible views have priority. + + _overlapInfo[viewCompatible] = _overlapInfo[fullyCompatible]; + _textureOverlaps[viewCompatible] = _textureOverlaps[fullyCompatible]; + + _overlapInfo[fullyCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); + _textureOverlaps[fullyCompatible] = overlap; + } + + fullyCompatible++; + } + + viewCompatible++; + + hasLayerViews |= overlap.Info.GetSlices() < texture.Info.GetSlices(); + hasMipViews |= overlap.Info.Levels < texture.Info.Levels; + } + else + { + bool dataOverlaps = texture.DataOverlaps(overlap, compatibility); + + if (!overlap.IsView && dataOverlaps && !incompatibleOverlaps.Exists(incompatible => incompatible.Group == overlap.Group)) + { + incompatibleOverlaps.Add(new TextureIncompatibleOverlap(overlap.Group, compatibility)); + } + + bool removeOverlap; + bool modified = overlap.CheckModified(false); + + if (overlapInCache || !setData) + { + if (!dataOverlaps) + { + // Allow textures to overlap if their data does not actually overlap. + // This typically happens when mip level subranges of a layered texture are used. (each texture fills the gaps of the others) + continue; + } + + // The overlap texture is going to contain garbage data after we draw, or is generally incompatible. + // The texture group will obtain copy dependencies for any subresources that are compatible between the two textures, + // but sometimes its data must be flushed regardless. + + // If the texture was modified since its last use, then that data is probably meant to go into this texture. + // If the data has been modified by the CPU, then it also shouldn't be flushed. + + bool flush = overlapInCache && !modified && overlap.AlwaysFlushOnOverlap; + + setData |= modified || flush; + + if (overlapInCache) + { + _cache.Remove(overlap, flush); + } + + removeOverlap = modified; + } + else + { + // If an incompatible overlapping texture has been modified, then it's data is likely destined for this texture, + // and the overlapped texture will contain garbage. In this case, it should be removed to save memory. + removeOverlap = modified; + } + + if (removeOverlap && overlap.Info.Target != Target.TextureBuffer) + { + overlap.RemoveFromPools(false); + } + } + } + + texture.InitializeGroup(hasLayerViews, hasMipViews, incompatibleOverlaps); + + // We need to synchronize before copying the old view data to the texture, + // otherwise the copied data would be overwritten by a future synchronization. + texture.InitializeData(false, setData); + + texture.Group.InitializeOverlaps(); + + for (int index = 0; index < viewCompatible; index++) + { + Texture overlap = _textureOverlaps[index]; + + OverlapInfo oInfo = _overlapInfo[index]; + + if (overlap.Group == texture.Group) + { + // If the texture group is equal, then this texture (or its parent) is already a view. + continue; + } + + // Note: If we allow different sizes for those overlaps, + // we need to make sure that the "info" has the correct size for the parent texture here. + // Since this is not allowed right now, we don't need to do it. + + TextureInfo overlapInfo = overlap.Info; + + if (texture.ScaleFactor != overlap.ScaleFactor) + { + // A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself. + // In that case, we prefer the higher scale only if our format is render-target-like, otherwise we scale the view down before copy. + + texture.PropagateScale(overlap); + } + + if (oInfo.Compatibility != TextureViewCompatibility.Full) + { + // Copy only compatibility, or target texture is already a view. + + overlap.SynchronizeMemory(); + texture.CreateCopyDependency(overlap, oInfo.FirstLayer, oInfo.FirstLevel, false); + } + else + { + TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor); + + ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel); + + overlap.SynchronizeMemory(); + + overlap.HostTexture.CopyTo(newView, 0, 0); + + overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel); + } + } + + texture.SynchronizeMemory(); + } + + // Sampler textures are managed by the texture pool, all other textures + // are managed by the auto delete cache. + if (!isSamplerTexture) + { + _cache.Add(texture); + } + + lock (_textures) + { + _textures.Add(texture); + } + + if (partiallyMapped) + { + lock (_partiallyMappedTextures) + { + _partiallyMappedTextures.Add(texture); + } + } + + ShrinkOverlapsBufferIfNeeded(); + + for (int i = 0; i < overlapsCount; i++) + { + _textureOverlaps[i].DecrementReferenceCount(); + } + + return texture; + } + + /// <summary> + /// Attempt to find a texture on the short duration cache. + /// </summary> + /// <param name="descriptor">The texture descriptor</param> + /// <returns>The texture if found, null otherwise</returns> + public Texture FindShortCache(in TextureDescriptor descriptor) + { + return _cache.FindShortCache(descriptor); + } + + /// <summary> + /// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null. + /// </summary> + /// <param name="memoryManager">GPU memory manager where the texture is mapped</param> + /// <param name="gpuVa">GPU virtual address of the texture</param> + /// <param name="bpp">Bytes per pixel</param> + /// <param name="stride">If <paramref name="linear"/> is true, should have the texture stride, otherwise ignored</param> + /// <param name="height">If <paramref name="linear"/> is false, should have the texture height, otherwise ignored</param> + /// <param name="xCount">Number of pixels to be copied per line</param> + /// <param name="yCount">Number of lines to be copied</param> + /// <param name="linear">True if the texture has a linear layout, false otherwise</param> + /// <param name="gobBlocksInY">If <paramref name="linear"/> is false, the amount of GOB blocks in the Y axis</param> + /// <param name="gobBlocksInZ">If <paramref name="linear"/> is false, the amount of GOB blocks in the Z axis</param> + /// <returns>A matching texture, or null if there is no match</returns> + public Texture FindTexture( + MemoryManager memoryManager, + ulong gpuVa, + int bpp, + int stride, + int height, + int xCount, + int yCount, + bool linear, + int gobBlocksInY, + int gobBlocksInZ) + { + ulong address = memoryManager.Translate(gpuVa); + + if (address == MemoryManager.PteUnmapped) + { + return null; + } + + int addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps); + Texture textureMatch = null; + + for (int i = 0; i < addressMatches; i++) + { + Texture texture = _textureOverlaps[i]; + FormatInfo format = texture.Info.FormatInfo; + + if (texture.Info.DepthOrLayers > 1 || texture.Info.Levels > 1 || texture.Info.FormatInfo.IsCompressed) + { + // Don't support direct buffer copies to anything that isn't a single 2D image, uncompressed. + continue; + } + + bool match; + + if (linear) + { + // Size is not available for linear textures. Use the stride and end of the copy region instead. + + match = texture.Info.IsLinear && texture.Info.Stride == stride && yCount == texture.Info.Height; + } + else + { + // Bpp may be a mismatch between the target texture and the param. + // Due to the way linear strided and block layouts work, widths can be multiplied by Bpp for comparison. + // Note: tex.Width is the aligned texture size. Prefer param.XCount, as the destination should be a texture with that exact size. + + bool sizeMatch = xCount * bpp == texture.Info.Width * format.BytesPerPixel && height == texture.Info.Height; + bool formatMatch = !texture.Info.IsLinear && + texture.Info.GobBlocksInY == gobBlocksInY && + texture.Info.GobBlocksInZ == gobBlocksInZ; + + match = sizeMatch && formatMatch; + } + + if (match) + { + if (textureMatch == null) + { + textureMatch = texture; + } + else if (texture.Group != textureMatch.Group) + { + return null; // It's ambiguous which texture should match between multiple choices, so leave it up to the slow path. + } + } + } + + return textureMatch; + } + + /// <summary> + /// Resizes the temporary buffer used for range list intersection results, if it has grown too much. + /// </summary> + private void ShrinkOverlapsBufferIfNeeded() + { + if (_textureOverlaps.Length > OverlapsBufferMaxCapacity) + { + Array.Resize(ref _textureOverlaps, OverlapsBufferMaxCapacity); + } + } + + /// <summary> + /// Gets a texture creation information from texture information. + /// This can be used to create new host textures. + /// </summary> + /// <param name="info">Texture information</param> + /// <param name="caps">GPU capabilities</param> + /// <param name="scale">Texture scale factor, to be applied to the texture size</param> + /// <returns>The texture creation information</returns> + public static TextureCreateInfo GetCreateInfo(TextureInfo info, Capabilities caps, float scale) + { + FormatInfo formatInfo = TextureCompatibility.ToHostCompatibleFormat(info, caps); + + if (info.Target == Target.TextureBuffer && !caps.SupportsSnormBufferTextureFormat) + { + // If the host does not support signed normalized formats, we use a signed integer format instead. + // The shader will need the appropriate conversion code to compensate. + switch (formatInfo.Format) + { + case Format.R8Snorm: + formatInfo = new FormatInfo(Format.R8Sint, 1, 1, 1, 1); + break; + case Format.R16Snorm: + formatInfo = new FormatInfo(Format.R16Sint, 1, 1, 2, 1); + break; + case Format.R8G8Snorm: + formatInfo = new FormatInfo(Format.R8G8Sint, 1, 1, 2, 2); + break; + case Format.R16G16Snorm: + formatInfo = new FormatInfo(Format.R16G16Sint, 1, 1, 4, 2); + break; + case Format.R8G8B8A8Snorm: + formatInfo = new FormatInfo(Format.R8G8B8A8Sint, 1, 1, 4, 4); + break; + case Format.R16G16B16A16Snorm: + formatInfo = new FormatInfo(Format.R16G16B16A16Sint, 1, 1, 8, 4); + break; + } + } + + int width = info.Width / info.SamplesInX; + int height = info.Height / info.SamplesInY; + + int depth = info.GetDepth() * info.GetLayers(); + + if (scale != 1f) + { + width = (int)MathF.Ceiling(width * scale); + height = (int)MathF.Ceiling(height * scale); + } + + return new TextureCreateInfo( + width, + height, + depth, + info.Levels, + info.Samples, + formatInfo.BlockWidth, + formatInfo.BlockHeight, + formatInfo.BytesPerPixel, + formatInfo.Format, + info.DepthStencilMode, + info.Target, + info.SwizzleR, + info.SwizzleG, + info.SwizzleB, + info.SwizzleA); + } + + /// <summary> + /// Removes a texture from the cache. + /// </summary> + /// <remarks> + /// This only removes the texture from the internal list, not from the auto-deletion cache. + /// It may still have live references after the removal. + /// </remarks> + /// <param name="texture">The texture to be removed</param> + public void RemoveTextureFromCache(Texture texture) + { + lock (_textures) + { + _textures.Remove(texture); + } + + lock (_partiallyMappedTextures) + { + _partiallyMappedTextures.Remove(texture); + } + } + + /// <summary> + /// Adds a texture to the short duration cache. This typically keeps it alive for two ticks. + /// </summary> + /// <param name="texture">Texture to add to the short cache</param> + /// <param name="descriptor">Last used texture descriptor</param> + public void AddShortCache(Texture texture, ref TextureDescriptor descriptor) + { + _cache.AddShortCache(texture, ref descriptor); + } + + /// <summary> + /// Removes a texture from the short duration cache. + /// </summary> + /// <param name="texture">Texture to remove from the short cache</param> + public void RemoveShortCache(Texture texture) + { + _cache.RemoveShortCache(texture); + } + + /// <summary> + /// Ticks periodic elements of the texture cache. + /// </summary> + public void Tick() + { + _cache.ProcessShortCache(); + } + + /// <summary> + /// Disposes all textures and samplers in the cache. + /// It's an error to use the texture cache after disposal. + /// </summary> + public void Dispose() + { + lock (_textures) + { + foreach (Texture texture in _textures) + { + texture.Dispose(); + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs new file mode 100644 index 00000000..e93ea0c0 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -0,0 +1,911 @@ +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Texture; +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture format compatibility checks. + /// </summary> + static class TextureCompatibility + { + private enum FormatClass + { + Unclassified, + Bc1Rgba, + Bc2, + Bc3, + Bc4, + Bc5, + Bc6, + Bc7, + Etc2Rgb, + Etc2Rgba, + Astc4x4, + Astc5x4, + Astc5x5, + Astc6x5, + Astc6x6, + Astc8x5, + Astc8x6, + Astc8x8, + Astc10x5, + Astc10x6, + Astc10x8, + Astc10x10, + Astc12x10, + Astc12x12 + } + + /// <summary> + /// Checks if a format is host incompatible. + /// </summary> + /// <remarks> + /// Host incompatible formats can't be used directly, the texture data needs to be converted + /// to a compatible format first. + /// </remarks> + /// <param name="info">Texture information</param> + /// <param name="caps">Host GPU capabilities</param> + /// <returns>True if the format is incompatible, false otherwise</returns> + public static bool IsFormatHostIncompatible(TextureInfo info, Capabilities caps) + { + Format originalFormat = info.FormatInfo.Format; + return ToHostCompatibleFormat(info, caps).Format != originalFormat; + } + + /// <summary> + /// Converts a incompatible format to a host compatible format, or return the format directly + /// if it is already host compatible. + /// </summary> + /// <remarks> + /// This can be used to convert a incompatible compressed format to the decompressor + /// output format. + /// </remarks> + /// <param name="info">Texture information</param> + /// <param name="caps">Host GPU capabilities</param> + /// <returns>A host compatible format</returns> + public static FormatInfo ToHostCompatibleFormat(TextureInfo info, Capabilities caps) + { + // The host API does not support those compressed formats. + // We assume software decompression will be done for those textures, + // and so we adjust the format here to match the decompressor output. + + if (!caps.SupportsAstcCompression) + { + if (info.FormatInfo.Format.IsAstcUnorm()) + { + return GraphicsConfig.EnableTextureRecompression + ? new FormatInfo(Format.Bc7Unorm, 4, 4, 16, 4) + : new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); + } + else if (info.FormatInfo.Format.IsAstcSrgb()) + { + return GraphicsConfig.EnableTextureRecompression + ? new FormatInfo(Format.Bc7Srgb, 4, 4, 16, 4) + : new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4); + } + } + + if (!HostSupportsBcFormat(info.FormatInfo.Format, info.Target, caps)) + { + switch (info.FormatInfo.Format) + { + case Format.Bc1RgbaSrgb: + case Format.Bc2Srgb: + case Format.Bc3Srgb: + case Format.Bc7Srgb: + return new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4); + case Format.Bc1RgbaUnorm: + case Format.Bc2Unorm: + case Format.Bc3Unorm: + case Format.Bc7Unorm: + return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); + case Format.Bc4Unorm: + return new FormatInfo(Format.R8Unorm, 1, 1, 1, 1); + case Format.Bc4Snorm: + return new FormatInfo(Format.R8Snorm, 1, 1, 1, 1); + case Format.Bc5Unorm: + return new FormatInfo(Format.R8G8Unorm, 1, 1, 2, 2); + case Format.Bc5Snorm: + return new FormatInfo(Format.R8G8Snorm, 1, 1, 2, 2); + case Format.Bc6HSfloat: + case Format.Bc6HUfloat: + return new FormatInfo(Format.R16G16B16A16Float, 1, 1, 8, 4); + } + } + + if (!caps.SupportsEtc2Compression) + { + switch (info.FormatInfo.Format) + { + case Format.Etc2RgbaSrgb: + case Format.Etc2RgbPtaSrgb: + case Format.Etc2RgbSrgb: + return new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4); + case Format.Etc2RgbaUnorm: + case Format.Etc2RgbPtaUnorm: + case Format.Etc2RgbUnorm: + return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); + } + } + + if (!caps.SupportsR4G4Format && info.FormatInfo.Format == Format.R4G4Unorm) + { + if (caps.SupportsR4G4B4A4Format) + { + return new FormatInfo(Format.R4G4B4A4Unorm, 1, 1, 2, 4); + } + else + { + return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); + } + } + + if (info.FormatInfo.Format == Format.R4G4B4A4Unorm) + { + if (!caps.SupportsR4G4B4A4Format) + { + return new FormatInfo(Format.R8G8B8A8Unorm, 1, 1, 4, 4); + } + } + else if (!caps.Supports5BitComponentFormat && info.FormatInfo.Format.Is16BitPacked()) + { + return new FormatInfo(info.FormatInfo.Format.IsBgr() ? Format.B8G8R8A8Unorm : Format.R8G8B8A8Unorm, 1, 1, 4, 4); + } + + return info.FormatInfo; + } + + /// <summary> + /// Checks if the host API supports a given texture compression format of the BC family. + /// </summary> + /// <param name="format">BC format to be checked</param> + /// <param name="target">Target usage of the texture</param> + /// <param name="caps">Host GPU Capabilities</param> + /// <returns>True if the texture host supports the format with the given target usage, false otherwise</returns> + public static bool HostSupportsBcFormat(Format format, Target target, Capabilities caps) + { + bool not3DOr3DCompressionSupported = target != Target.Texture3D || caps.Supports3DTextureCompression; + + switch (format) + { + case Format.Bc1RgbaSrgb: + case Format.Bc1RgbaUnorm: + case Format.Bc2Srgb: + case Format.Bc2Unorm: + case Format.Bc3Srgb: + case Format.Bc3Unorm: + return caps.SupportsBc123Compression && not3DOr3DCompressionSupported; + case Format.Bc4Unorm: + case Format.Bc4Snorm: + case Format.Bc5Unorm: + case Format.Bc5Snorm: + return caps.SupportsBc45Compression && not3DOr3DCompressionSupported; + case Format.Bc6HSfloat: + case Format.Bc6HUfloat: + case Format.Bc7Srgb: + case Format.Bc7Unorm: + return caps.SupportsBc67Compression && not3DOr3DCompressionSupported; + } + + return true; + } + + /// <summary> + /// Determines whether a texture can flush its data back to guest memory. + /// </summary> + /// <param name="info">Texture information</param> + /// <param name="caps">Host GPU Capabilities</param> + /// <returns>True if the texture can flush, false otherwise</returns> + public static bool CanTextureFlush(TextureInfo info, Capabilities caps) + { + if (IsFormatHostIncompatible(info, caps)) + { + return false; // Flushing this format is not supported, as it may have been converted to another host format. + } + + if (info.Target == Target.Texture2DMultisample || + info.Target == Target.Texture2DMultisampleArray) + { + return false; // Flushing multisample textures is not supported, the host does not allow getting their data. + } + + return true; + } + + /// <summary> + /// Checks if the texture format matches with the specified texture information. + /// </summary> + /// <param name="lhs">Texture information to compare</param> + /// <param name="rhs">Texture information to compare with</param> + /// <param name="forSampler">Indicates that the texture will be used for shader sampling</param> + /// <param name="forCopy">Indicates that the texture will be used as copy source or target</param> + /// <returns>A value indicating how well the formats match</returns> + public static TextureMatchQuality FormatMatches(TextureInfo lhs, TextureInfo rhs, bool forSampler, bool forCopy) + { + // D32F and R32F texture have the same representation internally, + // however the R32F format is used to sample from depth textures. + if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || forCopy)) + { + return TextureMatchQuality.FormatAlias; + } + + if (forCopy) + { + // The 2D engine does not support depth-stencil formats, so it will instead + // use equivalent color formats. We must also consider them as compatible. + if (lhs.FormatInfo.Format == Format.S8Uint && rhs.FormatInfo.Format == Format.R8Unorm) + { + return TextureMatchQuality.FormatAlias; + } + + if (lhs.FormatInfo.Format == Format.D16Unorm && rhs.FormatInfo.Format == Format.R16Unorm) + { + return TextureMatchQuality.FormatAlias; + } + + if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint || + lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm) + { + return TextureMatchQuality.FormatAlias; + } + } + + return lhs.FormatInfo.Format == rhs.FormatInfo.Format ? TextureMatchQuality.Perfect : TextureMatchQuality.NoMatch; + } + + /// <summary> + /// Checks if the texture layout specified matches with this texture layout. + /// The layout information is composed of the Stride for linear textures, or GOB block size + /// for block linear textures. + /// </summary> + /// <param name="lhs">Texture information to compare</param> + /// <param name="rhs">Texture information to compare with</param> + /// <returns>True if the layout matches, false otherwise</returns> + public static bool LayoutMatches(TextureInfo lhs, TextureInfo rhs) + { + if (lhs.IsLinear != rhs.IsLinear) + { + return false; + } + + // For linear textures, gob block sizes are ignored. + // For block linear textures, the stride is ignored. + if (rhs.IsLinear) + { + return lhs.Stride == rhs.Stride; + } + else + { + return lhs.GobBlocksInY == rhs.GobBlocksInY && + lhs.GobBlocksInZ == rhs.GobBlocksInZ; + } + } + + /// <summary> + /// Obtain the minimum compatibility level of two provided view compatibility results. + /// </summary> + /// <param name="first">The first compatibility level</param> + /// <param name="second">The second compatibility level</param> + /// <returns>The minimum compatibility level of two provided view compatibility results</returns> + public static TextureViewCompatibility PropagateViewCompatibility(TextureViewCompatibility first, TextureViewCompatibility second) + { + if (first == TextureViewCompatibility.Incompatible || second == TextureViewCompatibility.Incompatible) + { + return TextureViewCompatibility.Incompatible; + } + else if (first == TextureViewCompatibility.LayoutIncompatible || second == TextureViewCompatibility.LayoutIncompatible) + { + return TextureViewCompatibility.LayoutIncompatible; + } + else if (first == TextureViewCompatibility.CopyOnly || second == TextureViewCompatibility.CopyOnly) + { + return TextureViewCompatibility.CopyOnly; + } + else + { + return TextureViewCompatibility.Full; + } + } + + /// <summary> + /// Checks if the sizes of two texture levels are copy compatible. + /// </summary> + /// <param name="lhs">Texture information of the texture view</param> + /// <param name="rhs">Texture information of the texture view to match against</param> + /// <param name="lhsLevel">Mipmap level of the texture view in relation to this texture</param> + /// <param name="rhsLevel">Mipmap level of the texture view in relation to the second texture</param> + /// <returns>True if both levels are view compatible</returns> + public static bool CopySizeMatches(TextureInfo lhs, TextureInfo rhs, int lhsLevel, int rhsLevel) + { + Size size = GetAlignedSize(lhs, lhsLevel); + + Size otherSize = GetAlignedSize(rhs, rhsLevel); + + if (size.Width == otherSize.Width && size.Height == otherSize.Height) + { + return true; + } + else if (lhs.IsLinear && rhs.IsLinear) + { + // Copy between linear textures with matching stride. + int stride = BitUtils.AlignUp(Math.Max(1, lhs.Stride >> lhsLevel), Constants.StrideAlignment); + + return stride == rhs.Stride; + } + else + { + return false; + } + } + + /// <summary> + /// Checks if the sizes of two given textures are view compatible. + /// </summary> + /// <param name="lhs">Texture information of the texture view</param> + /// <param name="rhs">Texture information of the texture view to match against</param> + /// <param name="exact">Indicates if the sizes must be exactly equal</param> + /// <param name="level">Mipmap level of the texture view in relation to this texture</param> + /// <returns>The view compatibility level of the view sizes</returns> + public static TextureViewCompatibility ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, bool exact, int level) + { + Size lhsAlignedSize = GetAlignedSize(lhs, level); + Size rhsAlignedSize = GetAlignedSize(rhs); + + Size lhsSize = GetSizeInBlocks(lhs, level); + Size rhsSize = GetSizeInBlocks(rhs); + + bool alignedWidthMatches = lhsAlignedSize.Width == rhsAlignedSize.Width; + + if (lhs.FormatInfo.BytesPerPixel != rhs.FormatInfo.BytesPerPixel && IsIncompatibleFormatAliasingAllowed(lhs.FormatInfo, rhs.FormatInfo)) + { + alignedWidthMatches = lhsSize.Width * lhs.FormatInfo.BytesPerPixel == rhsSize.Width * rhs.FormatInfo.BytesPerPixel; + } + + TextureViewCompatibility result = TextureViewCompatibility.Full; + + // For copies, we can copy a subset of the 3D texture slices, + // so the depth may be different in this case. + if (rhs.Target == Target.Texture3D && lhsSize.Depth != rhsSize.Depth) + { + result = TextureViewCompatibility.CopyOnly; + } + + // Some APIs align the width for copy and render target textures, + // so the width may not match in this case for different uses of the same texture. + // To account for this, we compare the aligned width here. + // We expect height to always match exactly, if the texture is the same. + if (alignedWidthMatches && lhsSize.Height == rhsSize.Height) + { + return (exact && lhsSize.Width != rhsSize.Width) || lhsSize.Width < rhsSize.Width + ? TextureViewCompatibility.CopyOnly + : result; + } + else if (lhs.IsLinear && rhs.IsLinear && lhsSize.Height == rhsSize.Height) + { + // Copy between linear textures with matching stride. + int stride = BitUtils.AlignUp(Math.Max(1, lhs.Stride >> level), Constants.StrideAlignment); + + return stride == rhs.Stride ? TextureViewCompatibility.CopyOnly : TextureViewCompatibility.LayoutIncompatible; + } + else + { + return TextureViewCompatibility.LayoutIncompatible; + } + } + + /// <summary> + /// Checks if the potential child texture fits within the level and layer bounds of the parent. + /// </summary> + /// <param name="parent">Texture information for the parent</param> + /// <param name="child">Texture information for the child</param> + /// <param name="layer">Base layer of the child texture</param> + /// <param name="level">Base level of the child texture</param> + /// <returns>Full compatiblity if the child's layer and level count fit within the parent, incompatible otherwise</returns> + public static TextureViewCompatibility ViewSubImagesInBounds(TextureInfo parent, TextureInfo child, int layer, int level) + { + if (level + child.Levels <= parent.Levels && + layer + child.GetSlices() <= parent.GetSlices()) + { + return TextureViewCompatibility.Full; + } + else + { + return TextureViewCompatibility.LayoutIncompatible; + } + } + + /// <summary> + /// Checks if the texture sizes of the supplied texture informations match. + /// </summary> + /// <param name="lhs">Texture information to compare</param> + /// <param name="rhs">Texture information to compare with</param> + /// <param name="exact">Indicates if the size must be exactly equal between the textures, or if <paramref name="rhs"/> is allowed to be larger</param> + /// <returns>True if the sizes matches, false otherwise</returns> + public static bool SizeMatches(TextureInfo lhs, TextureInfo rhs, bool exact) + { + if (lhs.GetLayers() != rhs.GetLayers()) + { + return false; + } + + Size lhsSize = GetSizeInBlocks(lhs); + Size rhsSize = GetSizeInBlocks(rhs); + + if (exact || lhs.IsLinear || rhs.IsLinear) + { + return lhsSize.Width == rhsSize.Width && + lhsSize.Height == rhsSize.Height && + lhsSize.Depth == rhsSize.Depth; + } + else + { + Size lhsAlignedSize = GetAlignedSize(lhs); + Size rhsAlignedSize = GetAlignedSize(rhs); + + return lhsAlignedSize.Width == rhsAlignedSize.Width && + lhsSize.Width >= rhsSize.Width && + lhsSize.Height == rhsSize.Height && + lhsSize.Depth == rhsSize.Depth; + } + } + + /// <summary> + /// Gets the aligned sizes for the given dimensions, using the specified texture information. + /// The alignment depends on the texture layout and format bytes per pixel. + /// </summary> + /// <param name="info">Texture information to calculate the aligned size from</param> + /// <param name="width">The width to be aligned</param> + /// <param name="height">The height to be aligned</param> + /// <param name="depth">The depth to be aligned</param> + /// <returns>The aligned texture size</returns> + private static Size GetAlignedSize(TextureInfo info, int width, int height, int depth) + { + if (info.IsLinear) + { + return SizeCalculator.GetLinearAlignedSize( + width, + height, + info.FormatInfo.BlockWidth, + info.FormatInfo.BlockHeight, + info.FormatInfo.BytesPerPixel); + } + else + { + return SizeCalculator.GetBlockLinearAlignedSize( + width, + height, + depth, + info.FormatInfo.BlockWidth, + info.FormatInfo.BlockHeight, + info.FormatInfo.BytesPerPixel, + info.GobBlocksInY, + info.GobBlocksInZ, + info.GobBlocksInTileX); + } + } + + /// <summary> + /// Gets the aligned sizes of the specified texture information. + /// The alignment depends on the texture layout and format bytes per pixel. + /// </summary> + /// <param name="info">Texture information to calculate the aligned size from</param> + /// <param name="level">Mipmap level for texture views</param> + /// <returns>The aligned texture size</returns> + public static Size GetAlignedSize(TextureInfo info, int level = 0) + { + int width = Math.Max(1, info.Width >> level); + int height = Math.Max(1, info.Height >> level); + int depth = Math.Max(1, info.GetDepth() >> level); + + return GetAlignedSize(info, width, height, depth); + } + + /// <summary> + /// Gets the size in blocks for the given texture information. + /// For non-compressed formats, that's the same as the regular size. + /// </summary> + /// <param name="info">Texture information to calculate the aligned size from</param> + /// <param name="level">Mipmap level for texture views</param> + /// <returns>The texture size in blocks</returns> + public static Size GetSizeInBlocks(TextureInfo info, int level = 0) + { + int width = Math.Max(1, info.Width >> level); + int height = Math.Max(1, info.Height >> level); + int depth = Math.Max(1, info.GetDepth() >> level); + + return new Size( + BitUtils.DivRoundUp(width, info.FormatInfo.BlockWidth), + BitUtils.DivRoundUp(height, info.FormatInfo.BlockHeight), + depth); + } + + /// <summary> + /// Check if it's possible to create a view with the layout of the second texture information from the first. + /// The layout information is composed of the Stride for linear textures, or GOB block size + /// for block linear textures. + /// </summary> + /// <param name="lhs">Texture information of the texture view</param> + /// <param name="rhs">Texture information of the texture view to compare against</param> + /// <param name="level">Start level of the texture view, in relation with the first texture</param> + /// <returns>True if the layout is compatible, false otherwise</returns> + public static bool ViewLayoutCompatible(TextureInfo lhs, TextureInfo rhs, int level) + { + if (lhs.IsLinear != rhs.IsLinear) + { + return false; + } + + // For linear textures, gob block sizes are ignored. + // For block linear textures, the stride is ignored. + if (rhs.IsLinear) + { + int stride = Math.Max(1, lhs.Stride >> level); + stride = BitUtils.AlignUp(stride, Constants.StrideAlignment); + + return stride == rhs.Stride; + } + else + { + int height = Math.Max(1, lhs.Height >> level); + int depth = Math.Max(1, lhs.GetDepth() >> level); + + (int gobBlocksInY, int gobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes( + height, + depth, + lhs.FormatInfo.BlockHeight, + lhs.GobBlocksInY, + lhs.GobBlocksInZ); + + return gobBlocksInY == rhs.GobBlocksInY && + gobBlocksInZ == rhs.GobBlocksInZ; + } + } + + /// <summary> + /// Check if it's possible to create a view with the layout of the second texture information from the first. + /// The layout information is composed of the Stride for linear textures, or GOB block size + /// for block linear textures. + /// </summary> + /// <param name="lhs">Texture information of the texture view</param> + /// <param name="rhs">Texture information of the texture view to compare against</param> + /// <param name="lhsLevel">Start level of the texture view, in relation with the first texture</param> + /// <param name="rhsLevel">Start level of the texture view, in relation with the second texture</param> + /// <returns>True if the layout is compatible, false otherwise</returns> + public static bool ViewLayoutCompatible(TextureInfo lhs, TextureInfo rhs, int lhsLevel, int rhsLevel) + { + if (lhs.IsLinear != rhs.IsLinear) + { + return false; + } + + // For linear textures, gob block sizes are ignored. + // For block linear textures, the stride is ignored. + if (rhs.IsLinear) + { + int lhsStride = Math.Max(1, lhs.Stride >> lhsLevel); + lhsStride = BitUtils.AlignUp(lhsStride, Constants.StrideAlignment); + + int rhsStride = Math.Max(1, rhs.Stride >> rhsLevel); + rhsStride = BitUtils.AlignUp(rhsStride, Constants.StrideAlignment); + + return lhsStride == rhsStride; + } + else + { + int lhsHeight = Math.Max(1, lhs.Height >> lhsLevel); + int lhsDepth = Math.Max(1, lhs.GetDepth() >> lhsLevel); + + (int lhsGobBlocksInY, int lhsGobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes( + lhsHeight, + lhsDepth, + lhs.FormatInfo.BlockHeight, + lhs.GobBlocksInY, + lhs.GobBlocksInZ); + + int rhsHeight = Math.Max(1, rhs.Height >> rhsLevel); + int rhsDepth = Math.Max(1, rhs.GetDepth() >> rhsLevel); + + (int rhsGobBlocksInY, int rhsGobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes( + rhsHeight, + rhsDepth, + rhs.FormatInfo.BlockHeight, + rhs.GobBlocksInY, + rhs.GobBlocksInZ); + + return lhsGobBlocksInY == rhsGobBlocksInY && + lhsGobBlocksInZ == rhsGobBlocksInZ; + } + } + + /// <summary> + /// Checks if the view format of the first texture format is compatible with the format of the second. + /// In general, the formats are considered compatible if the bytes per pixel values are equal, + /// but there are more complex rules for some formats, like compressed or depth-stencil formats. + /// This follows the host API copy compatibility rules. + /// </summary> + /// <param name="lhs">Texture information of the texture view</param> + /// <param name="rhs">Texture information of the texture view</param> + /// <param name="caps">Host GPU capabilities</param> + /// <returns>The view compatibility level of the texture formats</returns> + public static TextureViewCompatibility ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs, Capabilities caps) + { + FormatInfo lhsFormat = lhs.FormatInfo; + FormatInfo rhsFormat = rhs.FormatInfo; + + if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil()) + { + return lhsFormat.Format == rhsFormat.Format ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible; + } + + if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps)) + { + return lhsFormat.Format == rhsFormat.Format ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible; + } + + if (lhsFormat.IsCompressed && rhsFormat.IsCompressed) + { + FormatClass lhsClass = GetFormatClass(lhsFormat.Format); + FormatClass rhsClass = GetFormatClass(rhsFormat.Format); + + return lhsClass == rhsClass ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible; + } + else if (lhsFormat.BytesPerPixel == rhsFormat.BytesPerPixel) + { + return lhs.FormatInfo.IsCompressed == rhs.FormatInfo.IsCompressed + ? TextureViewCompatibility.Full + : TextureViewCompatibility.CopyOnly; + } + else if (IsIncompatibleFormatAliasingAllowed(lhsFormat, rhsFormat)) + { + return TextureViewCompatibility.CopyOnly; + } + + return TextureViewCompatibility.Incompatible; + } + + /// <summary> + /// Checks if aliasing of two formats that would normally be considered incompatible be allowed, + /// using copy dependencies. + /// </summary> + /// <param name="lhsFormat">Format information of the first texture</param + /// <param name="rhsFormat">Format information of the second texture</param> + /// <returns>True if aliasing should be allowed, false otherwise</returns> + private static bool IsIncompatibleFormatAliasingAllowed(FormatInfo lhsFormat, FormatInfo rhsFormat) + { + // Some games will try to alias textures with incompatible foramts, with different BPP (bytes per pixel). + // We allow that in some cases as long Width * BPP is equal on both textures. + // This is very conservative right now as we want to avoid copies as much as possible, + // so we only consider the formats we have seen being aliased. + + if (rhsFormat.BytesPerPixel < lhsFormat.BytesPerPixel) + { + (lhsFormat, rhsFormat) = (rhsFormat, lhsFormat); + } + + return lhsFormat.Format == Format.R8Unorm && rhsFormat.Format == Format.R8G8B8A8Unorm; + } + + /// <summary> + /// Check if the target of the first texture view information is compatible with the target of the second texture view information. + /// This follows the host API target compatibility rules. + /// </summary> + /// <param name="lhs">Texture information of the texture view</param + /// <param name="rhs">Texture information of the texture view</param> + /// <param name="caps">Host GPU capabilities</param> + /// <returns>True if the targets are compatible, false otherwise</returns> + public static TextureViewCompatibility ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs, ref Capabilities caps) + { + bool result = false; + switch (lhs.Target) + { + case Target.Texture1D: + case Target.Texture1DArray: + result = rhs.Target == Target.Texture1D || + rhs.Target == Target.Texture1DArray; + break; + + case Target.Texture2D: + result = rhs.Target == Target.Texture2D || + rhs.Target == Target.Texture2DArray; + break; + + case Target.Texture2DArray: + result = rhs.Target == Target.Texture2D || + rhs.Target == Target.Texture2DArray; + + if (rhs.Target == Target.Cubemap || rhs.Target == Target.CubemapArray) + { + return caps.SupportsCubemapView ? TextureViewCompatibility.Full : TextureViewCompatibility.CopyOnly; + } + break; + case Target.Cubemap: + case Target.CubemapArray: + result = rhs.Target == Target.Cubemap || + rhs.Target == Target.CubemapArray; + + if (rhs.Target == Target.Texture2D || rhs.Target == Target.Texture2DArray) + { + return caps.SupportsCubemapView ? TextureViewCompatibility.Full : TextureViewCompatibility.CopyOnly; + } + break; + case Target.Texture2DMultisample: + case Target.Texture2DMultisampleArray: + if (rhs.Target == Target.Texture2D || rhs.Target == Target.Texture2DArray) + { + return TextureViewCompatibility.CopyOnly; + } + + result = rhs.Target == Target.Texture2DMultisample || + rhs.Target == Target.Texture2DMultisampleArray; + break; + + case Target.Texture3D: + if (rhs.Target == Target.Texture2D) + { + return TextureViewCompatibility.CopyOnly; + } + + result = rhs.Target == Target.Texture3D; + break; + } + + return result ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible; + } + + /// <summary> + /// Checks if a swizzle component in two textures functionally match, taking into account if the components are defined. + /// </summary> + /// <param name="lhs">Texture information to compare</param> + /// <param name="rhs">Texture information to compare with</param> + /// <param name="swizzleLhs">Swizzle component for the first texture</param> + /// <param name="swizzleRhs">Swizzle component for the second texture</param> + /// <param name="component">Component index, starting at 0 for red</param> + /// <returns>True if the swizzle components functionally match, false othersize</returns> + private static bool SwizzleComponentMatches(TextureInfo lhs, TextureInfo rhs, SwizzleComponent swizzleLhs, SwizzleComponent swizzleRhs, int component) + { + int lhsComponents = lhs.FormatInfo.Components; + int rhsComponents = rhs.FormatInfo.Components; + + if (lhsComponents == 4 && rhsComponents == 4) + { + return swizzleLhs == swizzleRhs; + } + + // Swizzles after the number of components a format defines are "undefined". + // We allow these to not be equal under certain circumstances. + // This can only happen when there are less than 4 components in a format. + // It tends to happen when float depth textures are sampled. + + bool lhsDefined = (swizzleLhs - SwizzleComponent.Red) < lhsComponents; + bool rhsDefined = (swizzleRhs - SwizzleComponent.Red) < rhsComponents; + + if (lhsDefined == rhsDefined) + { + // If both are undefined, return true. Otherwise just check if they're equal. + return lhsDefined ? swizzleLhs == swizzleRhs : true; + } + else + { + SwizzleComponent defined = lhsDefined ? swizzleLhs : swizzleRhs; + SwizzleComponent undefined = lhsDefined ? swizzleRhs : swizzleLhs; + + // Undefined swizzle can be matched by a forced value (0, 1), exact equality, or expected value. + // For example, R___ matches R001, RGBA but not RBGA. + return defined == undefined || defined < SwizzleComponent.Red || defined == SwizzleComponent.Red + component; + } + } + + /// <summary> + /// Checks if the texture shader sampling parameters of two texture informations match. + /// </summary> + /// <param name="lhs">Texture information to compare</param> + /// <param name="rhs">Texture information to compare with</param> + /// <returns>True if the texture shader sampling parameters matches, false otherwise</returns> + public static bool SamplerParamsMatches(TextureInfo lhs, TextureInfo rhs) + { + return lhs.DepthStencilMode == rhs.DepthStencilMode && + SwizzleComponentMatches(lhs, rhs, lhs.SwizzleR, rhs.SwizzleR, 0) && + SwizzleComponentMatches(lhs, rhs, lhs.SwizzleG, rhs.SwizzleG, 1) && + SwizzleComponentMatches(lhs, rhs, lhs.SwizzleB, rhs.SwizzleB, 2) && + SwizzleComponentMatches(lhs, rhs, lhs.SwizzleA, rhs.SwizzleA, 3); + } + + /// <summary> + /// Check if the texture target and samples count (for multisampled textures) matches. + /// </summary> + /// <param name="first">Texture information to compare with</param> + /// <param name="rhs">Texture information to compare with</param> + /// <returns>True if the texture target and samples count matches, false otherwise</returns> + public static bool TargetAndSamplesCompatible(TextureInfo lhs, TextureInfo rhs) + { + return lhs.Target == rhs.Target && + lhs.SamplesInX == rhs.SamplesInX && + lhs.SamplesInY == rhs.SamplesInY; + } + + /// <summary> + /// Gets the texture format class, for compressed textures, or Unclassified otherwise. + /// </summary> + /// <param name="format">The format</param> + /// <returns>Format class</returns> + private static FormatClass GetFormatClass(Format format) + { + switch (format) + { + case Format.Bc1RgbaSrgb: + case Format.Bc1RgbaUnorm: + return FormatClass.Bc1Rgba; + case Format.Bc2Srgb: + case Format.Bc2Unorm: + return FormatClass.Bc2; + case Format.Bc3Srgb: + case Format.Bc3Unorm: + return FormatClass.Bc3; + case Format.Bc4Snorm: + case Format.Bc4Unorm: + return FormatClass.Bc4; + case Format.Bc5Snorm: + case Format.Bc5Unorm: + return FormatClass.Bc5; + case Format.Bc6HSfloat: + case Format.Bc6HUfloat: + return FormatClass.Bc6; + case Format.Bc7Srgb: + case Format.Bc7Unorm: + return FormatClass.Bc7; + case Format.Etc2RgbSrgb: + case Format.Etc2RgbUnorm: + return FormatClass.Etc2Rgb; + case Format.Etc2RgbaSrgb: + case Format.Etc2RgbaUnorm: + return FormatClass.Etc2Rgba; + case Format.Astc4x4Srgb: + case Format.Astc4x4Unorm: + return FormatClass.Astc4x4; + case Format.Astc5x4Srgb: + case Format.Astc5x4Unorm: + return FormatClass.Astc5x4; + case Format.Astc5x5Srgb: + case Format.Astc5x5Unorm: + return FormatClass.Astc5x5; + case Format.Astc6x5Srgb: + case Format.Astc6x5Unorm: + return FormatClass.Astc6x5; + case Format.Astc6x6Srgb: + case Format.Astc6x6Unorm: + return FormatClass.Astc6x6; + case Format.Astc8x5Srgb: + case Format.Astc8x5Unorm: + return FormatClass.Astc8x5; + case Format.Astc8x6Srgb: + case Format.Astc8x6Unorm: + return FormatClass.Astc8x6; + case Format.Astc8x8Srgb: + case Format.Astc8x8Unorm: + return FormatClass.Astc8x8; + case Format.Astc10x5Srgb: + case Format.Astc10x5Unorm: + return FormatClass.Astc10x5; + case Format.Astc10x6Srgb: + case Format.Astc10x6Unorm: + return FormatClass.Astc10x6; + case Format.Astc10x8Srgb: + case Format.Astc10x8Unorm: + return FormatClass.Astc10x8; + case Format.Astc10x10Srgb: + case Format.Astc10x10Unorm: + return FormatClass.Astc10x10; + case Format.Astc12x10Srgb: + case Format.Astc12x10Unorm: + return FormatClass.Astc12x10; + case Format.Astc12x12Srgb: + case Format.Astc12x12Unorm: + return FormatClass.Astc12x12; + } + + return FormatClass.Unclassified; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureComponent.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureComponent.cs new file mode 100644 index 00000000..359069bc --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureComponent.cs @@ -0,0 +1,43 @@ +using Ryujinx.Graphics.GAL; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture swizzle color component. + /// </summary> + enum TextureComponent + { + Zero = 0, + Red = 2, + Green = 3, + Blue = 4, + Alpha = 5, + OneSI = 6, + OneF = 7 + } + + static class TextureComponentConverter + { + /// <summary> + /// Converts the texture swizzle color component enum to the respective Graphics Abstraction Layer enum. + /// </summary> + /// <param name="component">Texture swizzle color component</param> + /// <returns>Converted enum</returns> + public static SwizzleComponent Convert(this TextureComponent component) + { + switch (component) + { + case TextureComponent.Zero: return SwizzleComponent.Zero; + case TextureComponent.Red: return SwizzleComponent.Red; + case TextureComponent.Green: return SwizzleComponent.Green; + case TextureComponent.Blue: return SwizzleComponent.Blue; + case TextureComponent.Alpha: return SwizzleComponent.Alpha; + case TextureComponent.OneSI: + case TextureComponent.OneF: + return SwizzleComponent.One; + } + + return SwizzleComponent.Zero; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs new file mode 100644 index 00000000..269ddbd9 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs @@ -0,0 +1,37 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// One side of a two-way dependency between one texture view and another. + /// Contains a reference to the handle owning the dependency, and the other dependency. + /// </summary> + class TextureDependency + { + /// <summary> + /// The handle that owns this dependency. + /// </summary> + public TextureGroupHandle Handle; + + /// <summary> + /// The other dependency linked to this one, which belongs to another handle. + /// </summary> + public TextureDependency Other; + + /// <summary> + /// Create a new texture dependency. + /// </summary> + /// <param name="handle">The handle that owns the dependency</param> + public TextureDependency(TextureGroupHandle handle) + { + Handle = handle; + } + + /// <summary> + /// Signal that the owner of this dependency has been modified, + /// meaning that the other dependency's handle must defer a copy from it. + /// </summary> + public void SignalModified() + { + Other.Handle.DeferCopy(Handle); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureDescriptor.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureDescriptor.cs new file mode 100644 index 00000000..3e35f8d2 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureDescriptor.cs @@ -0,0 +1,273 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Maxwell texture descriptor, as stored on the GPU texture pool memory region. + /// </summary> + struct TextureDescriptor : ITextureDescriptor, IEquatable<TextureDescriptor> + { +#pragma warning disable CS0649 + public uint Word0; + public uint Word1; + public uint Word2; + public uint Word3; + public uint Word4; + public uint Word5; + public uint Word6; + public uint Word7; +#pragma warning restore CS0649 + + /// <summary> + /// Unpacks Maxwell texture format integer. + /// </summary> + /// <returns>The texture format integer</returns> + public uint UnpackFormat() + { + return Word0 & 0x8007ffff; + } + + /// <summary> + /// Unpacks the swizzle component for the texture red color channel. + /// </summary> + /// <returns>The swizzle component</returns> + public TextureComponent UnpackSwizzleR() + { + return(TextureComponent)((Word0 >> 19) & 7); + } + + /// <summary> + /// Unpacks the swizzle component for the texture green color channel. + /// </summary> + /// <returns>The swizzle component</returns> + public TextureComponent UnpackSwizzleG() + { + return(TextureComponent)((Word0 >> 22) & 7); + } + + /// <summary> + /// Unpacks the swizzle component for the texture blue color channel. + /// </summary> + /// <returns>The swizzle component</returns> + public TextureComponent UnpackSwizzleB() + { + return(TextureComponent)((Word0 >> 25) & 7); + } + + /// <summary> + /// Unpacks the swizzle component for the texture alpha color channel. + /// </summary> + /// <returns>The swizzle component</returns> + public TextureComponent UnpackSwizzleA() + { + return(TextureComponent)((Word0 >> 28) & 7); + } + + /// <summary> + /// Unpacks the 40-bits texture GPU virtual address. + /// </summary> + /// <returns>The GPU virtual address</returns> + public ulong UnpackAddress() + { + return Word1 | ((ulong)(Word2 & 0xffff) << 32); + } + + /// <summary> + /// Unpacks texture descriptor type for this texture descriptor. + /// This defines the texture layout, among other things. + /// </summary> + /// <returns>The texture descriptor type</returns> + public TextureDescriptorType UnpackTextureDescriptorType() + { + return (TextureDescriptorType)((Word2 >> 21) & 7); + } + + /// <summary> + /// Unpacks the texture stride (bytes per line) for linear textures only. + /// Always 32-bytes aligned. + /// </summary> + /// <returns>The linear texture stride</returns> + public int UnpackStride() + { + return (int)(Word3 & 0xffff) << 5; + } + + /// <summary> + /// Unpacks the GOB block size in X (width) for block linear textures. + /// Must be always 1, ignored by the GPU. + /// </summary> + /// <returns>THe GOB block X size</returns> + public int UnpackGobBlocksInX() + { + return 1 << (int)(Word3 & 7); + } + + /// <summary> + /// Unpacks the GOB block size in Y (height) for block linear textures. + /// Must be always a power of 2, with a maximum value of 32. + /// </summary> + /// <returns>THe GOB block Y size</returns> + public int UnpackGobBlocksInY() + { + return 1 << (int)((Word3 >> 3) & 7); + } + + /// <summary> + /// Unpacks the GOB block size in Z (depth) for block linear textures. + /// Must be always a power of 2, with a maximum value of 32. + /// Must be 1 for any texture target other than 3D textures. + /// </summary> + /// <returns>The GOB block Z size</returns> + public int UnpackGobBlocksInZ() + { + return 1 << (int)((Word3 >> 6) & 7); + } + + /// <summary> + /// Number of GOB blocks per tile in the X direction. + /// This is only used for sparse textures, should be 1 otherwise. + /// </summary> + /// <returns>The number of GOB blocks per tile</returns> + public int UnpackGobBlocksInTileX() + { + return 1 << (int)((Word3 >> 10) & 7); + } + + /// <summary> + /// Unpacks the number of mipmap levels of the texture. + /// </summary> + /// <returns>The number of mipmap levels</returns> + public int UnpackLevels() + { + return (int)(Word3 >> 28) + 1; + } + + /// <summary> + /// Unpack the base level texture width size. + /// </summary> + /// <returns>The texture width</returns> + public int UnpackWidth() + { + return (int)(Word4 & 0xffff) + 1; + } + + /// <summary> + /// Unpack the width of a buffer texture. + /// </summary> + /// <returns>The texture width</returns> + public int UnpackBufferTextureWidth() + { + return (int)((Word4 & 0xffff) | (Word3 << 16)) + 1; + } + + /// <summary> + /// Unpacks the texture sRGB format flag. + /// </summary> + /// <returns>True if the texture is sRGB, false otherwise</returns> + public bool UnpackSrgb() + { + return (Word4 & (1 << 22)) != 0; + } + + /// <summary> + /// Unpacks the texture target. + /// </summary> + /// <returns>The texture target</returns> + public TextureTarget UnpackTextureTarget() + { + return (TextureTarget)((Word4 >> 23) & 0xf); + } + + /// <summary> + /// Unpack the base level texture height size, or array layers for 1D array textures. + /// Should be ignored for 1D or buffer textures. + /// </summary> + /// <returns>The texture height or layers count</returns> + public int UnpackHeight() + { + return (int)(Word5 & 0xffff) + 1; + } + + /// <summary> + /// Unpack the base level texture depth size, number of array layers or cubemap faces. + /// The meaning of this value depends on the texture target. + /// </summary> + /// <returns>The texture depth, layer or faces count</returns> + public int UnpackDepth() + { + return (int)((Word5 >> 16) & 0x3fff) + 1; + } + + /// <summary> + /// Unpacks the texture coordinates normalized flag. + /// When this is true, texture coordinates are expected to be in the [0, 1] range on the shader. + /// When this is false, texture coordinates are expected to be in the [0, W], [0, H] and [0, D] range. + /// It must be set to false (by the guest driver) for rectangle textures. + /// </summary> + /// <returns>The texture coordinates normalized flag</returns> + public bool UnpackTextureCoordNormalized() + { + return (Word5 & (1 << 31)) != 0; + } + + /// <summary> + /// Unpacks the base mipmap level of the texture. + /// </summary> + /// <returns>The base mipmap level of the texture</returns> + public int UnpackBaseLevel() + { + return (int)(Word7 & 0xf); + } + + /// <summary> + /// Unpacks the maximum mipmap level (inclusive) of the texture. + /// Usually equal to Levels minus 1. + /// </summary> + /// <returns>The maximum mipmap level (inclusive) of the texture</returns> + public int UnpackMaxLevelInclusive() + { + return (int)((Word7 >> 4) & 0xf); + } + + /// <summary> + /// Unpacks the multisampled texture samples count in each direction. + /// Must be ignored for non-multisample textures. + /// </summary> + /// <returns>The multisample counts enum</returns> + public TextureMsaaMode UnpackTextureMsaaMode() + { + return (TextureMsaaMode)((Word7 >> 8) & 0xf); + } + + /// <summary> + /// Check if two descriptors are equal. + /// </summary> + /// <param name="other">The descriptor to compare against</param> + /// <returns>True if they are equal, false otherwise</returns> + public bool Equals(ref TextureDescriptor other) + { + return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).Equals(Unsafe.As<TextureDescriptor, Vector256<byte>>(ref other)); + } + + /// <summary> + /// Check if two descriptors are equal. + /// </summary> + /// <param name="other">The descriptor to compare against</param> + /// <returns>True if they are equal, false otherwise</returns> + public bool Equals(TextureDescriptor other) + { + return Equals(ref other); + } + + /// <summary> + /// Gets a hash code for this descriptor. + /// </summary> + /// <returns>The hash code for this descriptor.</returns> + public override int GetHashCode() + { + return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).GetHashCode(); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureDescriptorType.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureDescriptorType.cs new file mode 100644 index 00000000..8e7d40bb --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureDescriptorType.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// The texture descriptor type. + /// This specifies the texture memory layout. + /// The texture descriptor structure depends on the type. + /// </summary> + enum TextureDescriptorType + { + Buffer, + LinearColorKey, + Linear, + BlockLinear, + BlockLinearColorKey + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs new file mode 100644 index 00000000..234e7e8c --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -0,0 +1,1611 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Cpu.Tracking; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Texture; +using Ryujinx.Memory; +using Ryujinx.Memory.Range; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// An overlapping texture group with a given view compatibility. + /// </summary> + readonly struct TextureIncompatibleOverlap + { + public readonly TextureGroup Group; + public readonly TextureViewCompatibility Compatibility; + + /// <summary> + /// Create a new texture incompatible overlap. + /// </summary> + /// <param name="group">The group that is incompatible</param> + /// <param name="compatibility">The view compatibility for the group</param> + public TextureIncompatibleOverlap(TextureGroup group, TextureViewCompatibility compatibility) + { + Group = group; + Compatibility = compatibility; + } + } + + /// <summary> + /// A texture group represents a group of textures that belong to the same storage. + /// When views are created, this class will track memory accesses for them separately. + /// The group iteratively adds more granular tracking as views of different kinds are added. + /// Note that a texture group can be absorbed into another when it becomes a view parent. + /// </summary> + class TextureGroup : IDisposable + { + /// <summary> + /// Threshold of layers to force granular handles (and thus partial loading) on array/3D textures. + /// </summary> + private const int GranularLayerThreshold = 8; + + private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false); + + /// <summary> + /// The storage texture associated with this group. + /// </summary> + public Texture Storage { get; } + + /// <summary> + /// Indicates if the texture has copy dependencies. If true, then all modifications + /// must be signalled to the group, rather than skipping ones still to be flushed. + /// </summary> + public bool HasCopyDependencies { get; set; } + + /// <summary> + /// Indicates if this texture has any incompatible overlaps alive. + /// </summary> + public bool HasIncompatibleOverlaps => _incompatibleOverlaps.Count > 0; + + private readonly GpuContext _context; + private readonly PhysicalMemory _physicalMemory; + + private int[] _allOffsets; + private int[] _sliceSizes; + private bool _is3D; + private bool _hasMipViews; + private bool _hasLayerViews; + private int _layers; + private int _levels; + + private MultiRange TextureRange => Storage.Range; + + /// <summary> + /// The views list from the storage texture. + /// </summary> + private List<Texture> _views; + private TextureGroupHandle[] _handles; + private bool[] _loadNeeded; + + /// <summary> + /// Other texture groups that have incompatible overlaps with this one. + /// </summary> + private List<TextureIncompatibleOverlap> _incompatibleOverlaps; + private bool _incompatibleOverlapsDirty = true; + private bool _flushIncompatibleOverlaps; + + /// <summary> + /// Create a new texture group. + /// </summary> + /// <param name="context">GPU context that the texture group belongs to</param> + /// <param name="physicalMemory">Physical memory where the <paramref name="storage"/> texture is mapped</param> + /// <param name="storage">The storage texture for this group</param> + /// <param name="incompatibleOverlaps">Groups that overlap with this one but are incompatible</param> + public TextureGroup(GpuContext context, PhysicalMemory physicalMemory, Texture storage, List<TextureIncompatibleOverlap> incompatibleOverlaps) + { + Storage = storage; + _context = context; + _physicalMemory = physicalMemory; + + _is3D = storage.Info.Target == Target.Texture3D; + _layers = storage.Info.GetSlices(); + _levels = storage.Info.Levels; + + _incompatibleOverlaps = incompatibleOverlaps; + _flushIncompatibleOverlaps = TextureCompatibility.IsFormatHostIncompatible(storage.Info, context.Capabilities); + } + + /// <summary> + /// Initialize a new texture group's dirty regions and offsets. + /// </summary> + /// <param name="size">Size info for the storage texture</param> + /// <param name="hasLayerViews">True if the storage will have layer views</param> + /// <param name="hasMipViews">True if the storage will have mip views</param> + public void Initialize(ref SizeInfo size, bool hasLayerViews, bool hasMipViews) + { + _allOffsets = size.AllOffsets; + _sliceSizes = size.SliceSizes; + + if (Storage.Target.HasDepthOrLayers() && Storage.Info.GetSlices() > GranularLayerThreshold) + { + _hasLayerViews = true; + _hasMipViews = true; + } + else + { + (_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews); + + // If the texture is partially mapped, fully subdivide handles immediately. + + MultiRange range = Storage.Range; + for (int i = 0; i < range.Count; i++) + { + if (range.GetSubRange(i).Address == MemoryManager.PteUnmapped) + { + _hasLayerViews = true; + _hasMipViews = true; + + break; + } + } + } + + RecalculateHandleRegions(); + } + + /// <summary> + /// Initialize all incompatible overlaps in the list, registering them with the other texture groups + /// and creating copy dependencies when partially compatible. + /// </summary> + public void InitializeOverlaps() + { + foreach (TextureIncompatibleOverlap overlap in _incompatibleOverlaps) + { + if (overlap.Compatibility == TextureViewCompatibility.LayoutIncompatible) + { + CreateCopyDependency(overlap.Group, false); + } + + overlap.Group._incompatibleOverlaps.Add(new TextureIncompatibleOverlap(this, overlap.Compatibility)); + overlap.Group._incompatibleOverlapsDirty = true; + } + + if (_incompatibleOverlaps.Count > 0) + { + SignalIncompatibleOverlapModified(); + } + } + + /// <summary> + /// Signal that the group is dirty to all views and the storage. + /// </summary> + private void SignalAllDirty() + { + Storage.SignalGroupDirty(); + if (_views != null) + { + foreach (Texture texture in _views) + { + texture.SignalGroupDirty(); + } + } + } + + /// <summary> + /// Signal that an incompatible overlap has been modified. + /// If this group must flush incompatible overlaps, the group is signalled as dirty too. + /// </summary> + private void SignalIncompatibleOverlapModified() + { + _incompatibleOverlapsDirty = true; + + if (_flushIncompatibleOverlaps) + { + SignalAllDirty(); + } + } + + + /// <summary> + /// Flushes incompatible overlaps if the storage format requires it, and they have been modified. + /// This allows unsupported host formats to accept data written to format aliased textures. + /// </summary> + /// <returns>True if data was flushed, false otherwise</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool FlushIncompatibleOverlapsIfNeeded() + { + if (_flushIncompatibleOverlaps && _incompatibleOverlapsDirty) + { + bool flushed = false; + + foreach (var overlap in _incompatibleOverlaps) + { + flushed |= overlap.Group.Storage.FlushModified(true); + } + + _incompatibleOverlapsDirty = false; + + return flushed; + } + else + { + return false; + } + } + + /// <summary> + /// Check and optionally consume the dirty flags for a given texture. + /// The state is shared between views of the same layers and levels. + /// </summary> + /// <param name="texture">The texture being used</param> + /// <param name="consume">True to consume the dirty flags and reprotect, false to leave them as is</param> + /// <returns>True if a flag was dirty, false otherwise</returns> + public bool CheckDirty(Texture texture, bool consume) + { + bool dirty = false; + + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + foreach (CpuRegionHandle handle in group.Handles) + { + if (handle.Dirty) + { + if (consume) + { + handle.Reprotect(); + } + + dirty = true; + } + } + } + }); + + return dirty; + } + + /// <summary> + /// Synchronize memory for a given texture. + /// If overlapping tracking handles are dirty, fully or partially synchronize the texture data. + /// </summary> + /// <param name="texture">The texture being used</param> + public void SynchronizeMemory(Texture texture) + { + FlushIncompatibleOverlapsIfNeeded(); + + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + bool dirty = false; + bool anyModified = false; + bool anyNotDirty = false; + + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + bool modified = group.Modified; + bool handleDirty = false; + bool handleUnmapped = false; + + foreach (CpuRegionHandle handle in group.Handles) + { + if (handle.Dirty) + { + handle.Reprotect(); + handleDirty = true; + } + else + { + handleUnmapped |= handle.Unmapped; + } + } + + // If the modified flag is still present, prefer the data written from gpu. + // A write from CPU will do a flush before writing its data, which should unset this. + if (modified) + { + handleDirty = false; + } + + // Evaluate if any copy dependencies need to be fulfilled. A few rules: + // If the copy handle needs to be synchronized, prefer our own state. + // If we need to be synchronized and there is a copy present, prefer the copy. + + if (group.NeedsCopy && group.Copy(_context)) + { + anyModified |= true; // The copy target has been modified. + handleDirty = false; + } + else + { + anyModified |= modified; + dirty |= handleDirty; + } + + if (group.NeedsCopy) + { + // The texture we copied from is still being written to. Copy from it again the next time this texture is used. + texture.SignalGroupDirty(); + } + + bool loadNeeded = handleDirty && !handleUnmapped; + + anyNotDirty |= !loadNeeded; + _loadNeeded[baseHandle + i] = loadNeeded; + } + + if (dirty) + { + if (anyNotDirty || (_handles.Length > 1 && (anyModified || split))) + { + // Partial texture invalidation. Only update the layers/levels with dirty flags of the storage. + + SynchronizePartial(baseHandle, regionCount); + } + else + { + // Full texture invalidation. + + texture.SynchronizeFull(); + } + } + }); + } + + /// <summary> + /// Synchronize part of the storage texture, represented by a given range of handles. + /// Only handles marked by the _loadNeeded array will be synchronized. + /// </summary> + /// <param name="baseHandle">The base index of the range of handles</param> + /// <param name="regionCount">The number of handles to synchronize</param> + private void SynchronizePartial(int baseHandle, int regionCount) + { + int spanEndIndex = -1; + int spanBase = 0; + ReadOnlySpan<byte> dataSpan = ReadOnlySpan<byte>.Empty; + + for (int i = 0; i < regionCount; i++) + { + if (_loadNeeded[baseHandle + i]) + { + var info = GetHandleInformation(baseHandle + i); + + // Ensure the data for this handle is loaded in the span. + if (spanEndIndex <= i - 1) + { + spanEndIndex = i; + + if (_is3D) + { + // Look ahead to see how many handles need to be loaded. + for (int j = i + 1; j < regionCount; j++) + { + if (_loadNeeded[baseHandle + j]) + { + spanEndIndex = j; + } + else + { + break; + } + } + } + + var endInfo = spanEndIndex == i ? info : GetHandleInformation(baseHandle + spanEndIndex); + + spanBase = _allOffsets[info.Index]; + int spanLast = _allOffsets[endInfo.Index + endInfo.Layers * endInfo.Levels - 1]; + int endOffset = Math.Min(spanLast + _sliceSizes[endInfo.BaseLevel + endInfo.Levels - 1], (int)Storage.Size); + int size = endOffset - spanBase; + + dataSpan = _physicalMemory.GetSpan(Storage.Range.Slice((ulong)spanBase, (ulong)size)); + } + + // Only one of these will be greater than 1, as partial sync is only called when there are sub-image views. + for (int layer = 0; layer < info.Layers; layer++) + { + for (int level = 0; level < info.Levels; level++) + { + int offsetIndex = GetOffsetIndex(info.BaseLayer + layer, info.BaseLevel + level); + int offset = _allOffsets[offsetIndex]; + + ReadOnlySpan<byte> data = dataSpan.Slice(offset - spanBase); + + SpanOrArray<byte> result = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel + level, true); + + Storage.SetData(result, info.BaseLayer + layer, info.BaseLevel + level); + } + } + } + } + } + + /// <summary> + /// Synchronize dependent textures, if any of them have deferred a copy from the given texture. + /// </summary> + /// <param name="texture">The texture to synchronize dependents of</param> + public void SynchronizeDependents(Texture texture) + { + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + group.SynchronizeDependents(); + } + }); + } + + /// <summary> + /// Determines whether flushes in this texture group should be tracked. + /// Incompatible overlaps may need data from this texture to flush tracked for it to be visible to them. + /// </summary> + /// <returns>True if flushes should be tracked, false otherwise</returns> + private bool ShouldFlushTriggerTracking() + { + foreach (var overlap in _incompatibleOverlaps) + { + if (overlap.Group._flushIncompatibleOverlaps) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Gets data from the host GPU, and flushes a slice to guest memory. + /// </summary> + /// <remarks> + /// This method should be used to retrieve data that was modified by the host GPU. + /// This is not cheap, avoid doing that unless strictly needed. + /// When possible, the data is written directly into guest memory, rather than copied. + /// </remarks> + /// <param name="tracked">True if writing the texture data is tracked, false otherwise</param> + /// <param name="sliceIndex">The index of the slice to flush</param> + /// <param name="texture">The specific host texture to flush. Defaults to the storage texture</param> + private void FlushTextureDataSliceToGuest(bool tracked, int sliceIndex, ITexture texture = null) + { + (int layer, int level) = GetLayerLevelForView(sliceIndex); + + int offset = _allOffsets[sliceIndex]; + int endOffset = Math.Min(offset + _sliceSizes[level], (int)Storage.Size); + int size = endOffset - offset; + + using WritableRegion region = _physicalMemory.GetWritableRegion(Storage.Range.Slice((ulong)offset, (ulong)size), tracked); + + Storage.GetTextureDataSliceFromGpu(region.Memory.Span, layer, level, tracked, texture); + } + + /// <summary> + /// Gets and flushes a number of slices of the storage texture to guest memory. + /// </summary> + /// <param name="tracked">True if writing the texture data is tracked, false otherwise</param> + /// <param name="sliceStart">The first slice to flush</param> + /// <param name="sliceEnd">The slice to finish flushing on (exclusive)</param> + /// <param name="texture">The specific host texture to flush. Defaults to the storage texture</param> + private void FlushSliceRange(bool tracked, int sliceStart, int sliceEnd, ITexture texture = null) + { + for (int i = sliceStart; i < sliceEnd; i++) + { + FlushTextureDataSliceToGuest(tracked, i, texture); + } + } + + /// <summary> + /// Flush modified ranges for a given texture. + /// </summary> + /// <param name="texture">The texture being used</param> + /// <param name="tracked">True if the flush writes should be tracked, false otherwise</param> + /// <returns>True if data was flushed, false otherwise</returns> + public bool FlushModified(Texture texture, bool tracked) + { + tracked = tracked || ShouldFlushTriggerTracking(); + bool flushed = false; + + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + int startSlice = 0; + int endSlice = 0; + bool allModified = true; + + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + if (group.Modified) + { + if (endSlice < group.BaseSlice) + { + if (endSlice > startSlice) + { + FlushSliceRange(tracked, startSlice, endSlice); + flushed = true; + } + + startSlice = group.BaseSlice; + } + + endSlice = group.BaseSlice + group.SliceCount; + + if (tracked) + { + group.Modified = false; + + foreach (Texture texture in group.Overlaps) + { + texture.SignalModifiedDirty(); + } + } + } + else + { + allModified = false; + } + } + + if (endSlice > startSlice) + { + if (allModified && !split) + { + texture.Flush(tracked); + } + else + { + FlushSliceRange(tracked, startSlice, endSlice); + } + + flushed = true; + } + }); + + Storage.SignalModifiedDirty(); + + return flushed; + } + + /// <summary> + /// Clears competing modified flags for all incompatible ranges, if they have possibly been modified. + /// </summary> + /// <param name="texture">The texture that has been modified</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearIncompatibleOverlaps(Texture texture) + { + if (_incompatibleOverlapsDirty) + { + foreach (TextureIncompatibleOverlap incompatible in _incompatibleOverlaps) + { + incompatible.Group.ClearModified(texture.Range, this); + + incompatible.Group.SignalIncompatibleOverlapModified(); + } + + _incompatibleOverlapsDirty = false; + } + } + + /// <summary> + /// Signal that a texture in the group has been modified by the GPU. + /// </summary> + /// <param name="texture">The texture that has been modified</param> + public void SignalModified(Texture texture) + { + ClearIncompatibleOverlaps(texture); + + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + group.SignalModified(_context); + } + }); + } + + /// <summary> + /// Signal that a texture in the group is actively bound, or has been unbound by the GPU. + /// </summary> + /// <param name="texture">The texture that has been modified</param> + /// <param name="bound">True if this texture is being bound, false if unbound</param> + public void SignalModifying(Texture texture, bool bound) + { + ClearIncompatibleOverlaps(texture); + + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + group.SignalModifying(bound, _context); + } + }); + } + + /// <summary> + /// Register a read/write action to flush for a texture group. + /// </summary> + /// <param name="group">The group to register an action for</param> + public void RegisterAction(TextureGroupHandle group) + { + foreach (CpuRegionHandle handle in group.Handles) + { + handle.RegisterAction((address, size) => FlushAction(group, address, size)); + } + } + + /// <summary> + /// Propagates the mip/layer view flags depending on the texture type. + /// When the most granular type of subresource has views, the other type of subresource must be segmented granularly too. + /// </summary> + /// <param name="hasLayerViews">True if the storage has layer views</param> + /// <param name="hasMipViews">True if the storage has mip views</param> + /// <returns>The input values after propagation</returns> + private (bool HasLayerViews, bool HasMipViews) PropagateGranularity(bool hasLayerViews, bool hasMipViews) + { + if (_is3D) + { + hasMipViews |= hasLayerViews; + } + else + { + hasLayerViews |= hasMipViews; + } + + return (hasLayerViews, hasMipViews); + } + + /// <summary> + /// Evaluate the range of tracking handles which a view texture overlaps with. + /// </summary> + /// <param name="texture">The texture to get handles for</param> + /// <param name="callback"> + /// A function to be called with the base index of the range of handles for the given texture, and the number of handles it covers. + /// This can be called for multiple disjoint ranges, if required. + /// </param> + private void EvaluateRelevantHandles(Texture texture, HandlesCallbackDelegate callback) + { + if (texture == Storage || !(_hasMipViews || _hasLayerViews)) + { + callback(0, _handles.Length); + + return; + } + + EvaluateRelevantHandles(texture.FirstLayer, texture.FirstLevel, texture.Info.GetSlices(), texture.Info.Levels, callback); + } + + /// <summary> + /// Evaluate the range of tracking handles which a view texture overlaps with, + /// using the view's position and slice/level counts. + /// </summary> + /// <param name="firstLayer">The first layer of the texture</param> + /// <param name="firstLevel">The first level of the texture</param> + /// <param name="slices">The slice count of the texture</param> + /// <param name="levels">The level count of the texture</param> + /// <param name="callback"> + /// A function to be called with the base index of the range of handles for the given texture, and the number of handles it covers. + /// This can be called for multiple disjoint ranges, if required. + /// </param> + private void EvaluateRelevantHandles(int firstLayer, int firstLevel, int slices, int levels, HandlesCallbackDelegate callback) + { + int targetLayerHandles = _hasLayerViews ? slices : 1; + int targetLevelHandles = _hasMipViews ? levels : 1; + + if (_is3D) + { + // Future mip levels come after all layers of the last mip level. Each mipmap has less layers (depth) than the last. + + if (!_hasLayerViews) + { + // When there are no layer views, the mips are at a consistent offset. + + callback(firstLevel, targetLevelHandles); + } + else + { + (int levelIndex, int layerCount) = Get3DLevelRange(firstLevel); + + if (levels > 1 && slices < _layers) + { + // The given texture only covers some of the depth of multiple mips. (a "depth slice") + // Callback with each mip's range separately. + // Can assume that the group is fully subdivided (both slices and levels > 1 for storage) + + while (levels-- > 1) + { + callback(firstLayer + levelIndex, slices); + + levelIndex += layerCount; + layerCount = Math.Max(layerCount >> 1, 1); + slices = Math.Max(layerCount >> 1, 1); + } + } + else + { + int totalSize = Math.Min(layerCount, slices); + + while (levels-- > 1) + { + layerCount = Math.Max(layerCount >> 1, 1); + totalSize += layerCount; + } + + callback(firstLayer + levelIndex, totalSize); + } + } + } + else + { + // Future layers come after all mipmaps of the last. + int levelHandles = _hasMipViews ? _levels : 1; + + if (slices > 1 && levels < _levels) + { + // The given texture only covers some of the mipmaps of multiple slices. (a "mip slice") + // Callback with each layer's range separately. + // Can assume that the group is fully subdivided (both slices and levels > 1 for storage) + + for (int i = 0; i < slices; i++) + { + callback(firstLevel + (firstLayer + i) * levelHandles, targetLevelHandles, true); + } + } + else + { + callback(firstLevel + firstLayer * levelHandles, targetLevelHandles + (targetLayerHandles - 1) * levelHandles); + } + } + } + + /// <summary> + /// Get the range of offsets for a given mip level of a 3D texture. + /// </summary> + /// <param name="level">The level to return</param> + /// <returns>Start index and count of offsets for the given level</returns> + private (int Index, int Count) Get3DLevelRange(int level) + { + int index = 0; + int count = _layers; // Depth. Halves with each mip level. + + while (level-- > 0) + { + index += count; + count = Math.Max(count >> 1, 1); + } + + return (index, count); + } + + /// <summary> + /// Get view information for a single tracking handle. + /// </summary> + /// <param name="handleIndex">The index of the handle</param> + /// <returns>The layers and levels that the handle covers, and its index in the offsets array</returns> + private (int BaseLayer, int BaseLevel, int Levels, int Layers, int Index) GetHandleInformation(int handleIndex) + { + int baseLayer; + int baseLevel; + int levels = _hasMipViews ? 1 : _levels; + int layers = _hasLayerViews ? 1 : _layers; + int index; + + if (_is3D) + { + if (_hasLayerViews) + { + // NOTE: Will also have mip views, or only one level in storage. + + index = handleIndex; + baseLevel = 0; + + int levelLayers = _layers; + + while (handleIndex >= levelLayers) + { + handleIndex -= levelLayers; + baseLevel++; + levelLayers = Math.Max(levelLayers >> 1, 1); + } + + baseLayer = handleIndex; + } + else + { + baseLayer = 0; + baseLevel = handleIndex; + + (index, _) = Get3DLevelRange(baseLevel); + } + } + else + { + baseLevel = _hasMipViews ? handleIndex % _levels : 0; + baseLayer = _hasMipViews ? handleIndex / _levels : handleIndex; + index = baseLevel + baseLayer * _levels; + } + + return (baseLayer, baseLevel, levels, layers, index); + } + + /// <summary> + /// Gets the layer and level for a given view. + /// </summary> + /// <param name="index">The index of the view</param> + /// <returns>The layer and level of the specified view</returns> + private (int BaseLayer, int BaseLevel) GetLayerLevelForView(int index) + { + if (_is3D) + { + int baseLevel = 0; + + int levelLayers = _layers; + + while (index >= levelLayers) + { + index -= levelLayers; + baseLevel++; + levelLayers = Math.Max(levelLayers >> 1, 1); + } + + return (index, baseLevel); + } + else + { + return (index / _levels, index % _levels); + } + } + + /// <summary> + /// Find the byte offset of a given texture relative to the storage. + /// </summary> + /// <param name="texture">The texture to locate</param> + /// <returns>The offset of the texture in bytes</returns> + public int FindOffset(Texture texture) + { + return _allOffsets[GetOffsetIndex(texture.FirstLayer, texture.FirstLevel)]; + } + + /// <summary> + /// Find the offset index of a given layer and level. + /// </summary> + /// <param name="layer">The view layer</param> + /// <param name="level">The view level</param> + /// <returns>The offset index of the given layer and level</returns> + public int GetOffsetIndex(int layer, int level) + { + if (_is3D) + { + return layer + Get3DLevelRange(level).Index; + } + else + { + return level + layer * _levels; + } + } + + /// <summary> + /// The action to perform when a memory tracking handle is flipped to dirty. + /// This notifies overlapping textures that the memory needs to be synchronized. + /// </summary> + /// <param name="groupHandle">The handle that a dirty flag was set on</param> + private void DirtyAction(TextureGroupHandle groupHandle) + { + // Notify all textures that belong to this handle. + + Storage.SignalGroupDirty(); + + lock (groupHandle.Overlaps) + { + foreach (Texture overlap in groupHandle.Overlaps) + { + overlap.SignalGroupDirty(); + } + } + } + + /// <summary> + /// Generate a CpuRegionHandle for a given address and size range in CPU VA. + /// </summary> + /// <param name="address">The start address of the tracked region</param> + /// <param name="size">The size of the tracked region</param> + /// <returns>A CpuRegionHandle covering the given range</returns> + private CpuRegionHandle GenerateHandle(ulong address, ulong size) + { + return _physicalMemory.BeginTracking(address, size, ResourceKind.Texture); + } + + /// <summary> + /// Generate a TextureGroupHandle covering a specified range of views. + /// </summary> + /// <param name="viewStart">The start view of the handle</param> + /// <param name="views">The number of views to cover</param> + /// <returns>A TextureGroupHandle covering the given views</returns> + private TextureGroupHandle GenerateHandles(int viewStart, int views) + { + int viewEnd = viewStart + views - 1; + (_, int lastLevel) = GetLayerLevelForView(viewEnd); + + int offset = _allOffsets[viewStart]; + int endOffset = _allOffsets[viewEnd] + _sliceSizes[lastLevel]; + int size = endOffset - offset; + + var result = new List<CpuRegionHandle>(); + + for (int i = 0; i < TextureRange.Count; i++) + { + MemoryRange item = TextureRange.GetSubRange(i); + int subRangeSize = (int)item.Size; + + int sliceStart = Math.Clamp(offset, 0, subRangeSize); + int sliceEnd = Math.Clamp(endOffset, 0, subRangeSize); + + if (sliceStart != sliceEnd && item.Address != MemoryManager.PteUnmapped) + { + result.Add(GenerateHandle(item.Address + (ulong)sliceStart, (ulong)(sliceEnd - sliceStart))); + } + + offset -= subRangeSize; + endOffset -= subRangeSize; + + if (endOffset <= 0) + { + break; + } + } + + (int firstLayer, int firstLevel) = GetLayerLevelForView(viewStart); + + if (_hasLayerViews && _hasMipViews) + { + size = _sliceSizes[firstLevel]; + } + + offset = _allOffsets[viewStart]; + ulong maxSize = Storage.Size - (ulong)offset; + + var groupHandle = new TextureGroupHandle( + this, + offset, + Math.Min(maxSize, (ulong)size), + _views, + firstLayer, + firstLevel, + viewStart, + views, + result.ToArray()); + + foreach (CpuRegionHandle handle in result) + { + handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); + } + + return groupHandle; + } + + /// <summary> + /// Update the views in this texture group, rebuilding the memory tracking if required. + /// </summary> + /// <param name="views">The views list of the storage texture</param> + /// <param name="texture">The texture that has been added, if that is the only change, otherwise null</param> + public void UpdateViews(List<Texture> views, Texture texture) + { + // This is saved to calculate overlapping views for each handle. + _views = views; + + bool layerViews = _hasLayerViews; + bool mipViews = _hasMipViews; + bool regionsRebuilt = false; + + if (!(layerViews && mipViews)) + { + foreach (Texture view in views) + { + if (view.Info.GetSlices() < _layers) + { + layerViews = true; + } + + if (view.Info.Levels < _levels) + { + mipViews = true; + } + } + + (layerViews, mipViews) = PropagateGranularity(layerViews, mipViews); + + if (layerViews != _hasLayerViews || mipViews != _hasMipViews) + { + _hasLayerViews = layerViews; + _hasMipViews = mipViews; + + RecalculateHandleRegions(); + regionsRebuilt = true; + } + } + + if (!regionsRebuilt) + { + if (texture != null) + { + int offset = FindOffset(texture); + + foreach (TextureGroupHandle handle in _handles) + { + handle.AddOverlap(offset, texture); + } + } + else + { + // Must update the overlapping views on all handles, but only if they were not just recreated. + + foreach (TextureGroupHandle handle in _handles) + { + handle.RecalculateOverlaps(this, views); + } + } + } + + SignalAllDirty(); + } + + + /// <summary> + /// Removes a view from the group, removing it from all overlap lists. + /// </summary> + /// <param name="view">View to remove from the group</param> + public void RemoveView(Texture view) + { + int offset = FindOffset(view); + + foreach (TextureGroupHandle handle in _handles) + { + handle.RemoveOverlap(offset, view); + } + } + + /// <summary> + /// Inherit handle state from an old set of handles, such as modified and dirty flags. + /// </summary> + /// <param name="oldHandles">The set of handles to inherit state from</param> + /// <param name="handles">The set of handles inheriting the state</param> + /// <param name="relativeOffset">The offset of the old handles in relation to the new ones</param> + private void InheritHandles(TextureGroupHandle[] oldHandles, TextureGroupHandle[] handles, int relativeOffset) + { + foreach (var group in handles) + { + foreach (var handle in group.Handles) + { + bool dirty = false; + + foreach (var oldGroup in oldHandles) + { + if (group.OverlapsWith(oldGroup.Offset + relativeOffset, oldGroup.Size)) + { + foreach (var oldHandle in oldGroup.Handles) + { + if (handle.OverlapsWith(oldHandle.Address, oldHandle.Size)) + { + dirty |= oldHandle.Dirty; + } + } + + group.Inherit(oldGroup, group.Offset == oldGroup.Offset + relativeOffset); + } + } + + if (dirty && !handle.Dirty) + { + handle.Reprotect(true); + } + + if (group.Modified) + { + handle.RegisterAction((address, size) => FlushAction(group, address, size)); + } + } + } + + foreach (var oldGroup in oldHandles) + { + oldGroup.Modified = false; + } + } + + /// <summary> + /// Inherit state from another texture group. + /// </summary> + /// <param name="other">The texture group to inherit from</param> + public void Inherit(TextureGroup other) + { + bool layerViews = _hasLayerViews || other._hasLayerViews; + bool mipViews = _hasMipViews || other._hasMipViews; + + if (layerViews != _hasLayerViews || mipViews != _hasMipViews) + { + _hasLayerViews = layerViews; + _hasMipViews = mipViews; + + RecalculateHandleRegions(); + } + + foreach (TextureIncompatibleOverlap incompatible in other._incompatibleOverlaps) + { + RegisterIncompatibleOverlap(incompatible, false); + + incompatible.Group._incompatibleOverlaps.RemoveAll(overlap => overlap.Group == other); + } + + int relativeOffset = Storage.Range.FindOffset(other.Storage.Range); + + InheritHandles(other._handles, _handles, relativeOffset); + } + + /// <summary> + /// Replace the current handles with the new handles. It is assumed that the new handles start dirty. + /// The dirty flags from the previous handles will be kept. + /// </summary> + /// <param name="handles">The handles to replace the current handles with</param> + /// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param> + private void ReplaceHandles(TextureGroupHandle[] handles, bool rangeChanged) + { + if (_handles != null) + { + // When replacing handles, they should start as non-dirty. + + foreach (TextureGroupHandle groupHandle in handles) + { + if (rangeChanged) + { + // When the storage range changes, this becomes a little different. + // If a range does not match one in the original, treat it as modified. + // It has been newly mapped and its data must be synchronized. + + if (groupHandle.Handles.Length == 0) + { + continue; + } + + foreach (var oldGroup in _handles) + { + if (!groupHandle.OverlapsWith(oldGroup.Offset, oldGroup.Size)) + { + continue; + } + + foreach (CpuRegionHandle handle in groupHandle.Handles) + { + bool hasMatch = false; + + foreach (var oldHandle in oldGroup.Handles) + { + if (oldHandle.RangeEquals(handle)) + { + hasMatch = true; + break; + } + } + + if (hasMatch) + { + handle.Reprotect(); + } + } + } + } + else + { + foreach (CpuRegionHandle handle in groupHandle.Handles) + { + handle.Reprotect(); + } + } + } + + InheritHandles(_handles, handles, 0); + + foreach (var oldGroup in _handles) + { + foreach (var oldHandle in oldGroup.Handles) + { + oldHandle.Dispose(); + } + } + } + + _handles = handles; + _loadNeeded = new bool[_handles.Length]; + } + + /// <summary> + /// Recalculate handle regions for this texture group, and inherit existing state into the new handles. + /// </summary> + /// <param name="rangeChanged">True if the storage memory range changed since the last region handle generation</param> + private void RecalculateHandleRegions(bool rangeChanged = false) + { + TextureGroupHandle[] handles; + + if (!(_hasMipViews || _hasLayerViews)) + { + // Single dirty region. + var cpuRegionHandles = new CpuRegionHandle[TextureRange.Count]; + int count = 0; + + for (int i = 0; i < TextureRange.Count; i++) + { + var currentRange = TextureRange.GetSubRange(i); + if (currentRange.Address != MemoryManager.PteUnmapped) + { + cpuRegionHandles[count++] = GenerateHandle(currentRange.Address, currentRange.Size); + } + } + + if (count != TextureRange.Count) + { + Array.Resize(ref cpuRegionHandles, count); + } + + var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, 0, _allOffsets.Length, cpuRegionHandles); + + foreach (CpuRegionHandle handle in cpuRegionHandles) + { + handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); + } + + handles = new TextureGroupHandle[] { groupHandle }; + } + else + { + // Get views for the host texture. + // It's worth noting that either the texture has layer views or mip views when getting to this point, which simplifies the logic a little. + // Depending on if the texture is 3d, either the mip views imply that layer views are present (2d) or the other way around (3d). + // This is enforced by the way the texture matched as a view, so we don't need to check. + + int layerHandles = _hasLayerViews ? _layers : 1; + int levelHandles = _hasMipViews ? _levels : 1; + + int handleIndex = 0; + + if (_is3D) + { + var handlesList = new List<TextureGroupHandle>(); + + for (int i = 0; i < levelHandles; i++) + { + for (int j = 0; j < layerHandles; j++) + { + (int viewStart, int views) = Get3DLevelRange(i); + viewStart += j; + views = _hasLayerViews ? 1 : views; // A layer view is also a mip view. + + handlesList.Add(GenerateHandles(viewStart, views)); + } + + layerHandles = Math.Max(1, layerHandles >> 1); + } + + handles = handlesList.ToArray(); + } + else + { + handles = new TextureGroupHandle[layerHandles * levelHandles]; + + for (int i = 0; i < layerHandles; i++) + { + for (int j = 0; j < levelHandles; j++) + { + int viewStart = j + i * _levels; + int views = _hasMipViews ? 1 : _levels; // A mip view is also a layer view. + + handles[handleIndex++] = GenerateHandles(viewStart, views); + } + } + } + } + + ReplaceHandles(handles, rangeChanged); + } + + /// <summary> + /// Regenerates handles when the storage range has been remapped. + /// This forces the regions to be fully subdivided. + /// </summary> + public void RangeChanged() + { + _hasLayerViews = true; + _hasMipViews = true; + + RecalculateHandleRegions(true); + + SignalAllDirty(); + } + + /// <summary> + /// Ensure that there is a handle for each potential texture view. Required for copy dependencies to work. + /// </summary> + private void EnsureFullSubdivision() + { + if (!(_hasLayerViews && _hasMipViews)) + { + _hasLayerViews = true; + _hasMipViews = true; + + RecalculateHandleRegions(); + } + } + + /// <summary> + /// Create a copy dependency between this texture group, and a texture at a given layer/level offset. + /// </summary> + /// <param name="other">The view compatible texture to create a dependency to</param> + /// <param name="firstLayer">The base layer of the given texture relative to the storage</param> + /// <param name="firstLevel">The base level of the given texture relative to the storage</param> + /// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param> + public void CreateCopyDependency(Texture other, int firstLayer, int firstLevel, bool copyTo) + { + TextureGroup otherGroup = other.Group; + + EnsureFullSubdivision(); + otherGroup.EnsureFullSubdivision(); + + // Get the location of each texture within its storage, so we can find the handles to apply the dependency to. + // This can consist of multiple disjoint regions, for example if this is a mip slice of an array texture. + + var targetRange = new List<(int BaseHandle, int RegionCount)>(); + var otherRange = new List<(int BaseHandle, int RegionCount)>(); + + EvaluateRelevantHandles(firstLayer, firstLevel, other.Info.GetSlices(), other.Info.Levels, (baseHandle, regionCount, split) => targetRange.Add((baseHandle, regionCount))); + otherGroup.EvaluateRelevantHandles(other, (baseHandle, regionCount, split) => otherRange.Add((baseHandle, regionCount))); + + int targetIndex = 0; + int otherIndex = 0; + (int Handle, int RegionCount) targetRegion = (0, 0); + (int Handle, int RegionCount) otherRegion = (0, 0); + + while (true) + { + if (targetRegion.RegionCount == 0) + { + if (targetIndex >= targetRange.Count) + { + break; + } + + targetRegion = targetRange[targetIndex++]; + } + + if (otherRegion.RegionCount == 0) + { + if (otherIndex >= otherRange.Count) + { + break; + } + + otherRegion = otherRange[otherIndex++]; + } + + TextureGroupHandle handle = _handles[targetRegion.Handle++]; + TextureGroupHandle otherHandle = other.Group._handles[otherRegion.Handle++]; + + targetRegion.RegionCount--; + otherRegion.RegionCount--; + + handle.CreateCopyDependency(otherHandle, copyTo); + + // If "copyTo" is true, this texture must copy to the other. + // Otherwise, it must copy to this texture. + + if (copyTo) + { + otherHandle.Copy(_context, handle); + } + else + { + handle.Copy(_context, otherHandle); + } + } + } + + /// <summary> + /// Creates a copy dependency to another texture group, where handles overlap. + /// Scans through all handles to find compatible patches in the other group. + /// </summary> + /// <param name="other">The texture group that overlaps this one</param> + /// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param> + public void CreateCopyDependency(TextureGroup other, bool copyTo) + { + for (int i = 0; i < _allOffsets.Length; i++) + { + (int layer, int level) = GetLayerLevelForView(i); + MultiRange handleRange = Storage.Range.Slice((ulong)_allOffsets[i], 1); + ulong handleBase = handleRange.GetSubRange(0).Address; + + for (int j = 0; j < other._handles.Length; j++) + { + (int otherLayer, int otherLevel) = other.GetLayerLevelForView(j); + MultiRange otherHandleRange = other.Storage.Range.Slice((ulong)other._allOffsets[j], 1); + ulong otherHandleBase = otherHandleRange.GetSubRange(0).Address; + + if (handleBase == otherHandleBase) + { + // Check if the two sizes are compatible. + TextureInfo info = Storage.Info; + TextureInfo otherInfo = other.Storage.Info; + + if (TextureCompatibility.ViewLayoutCompatible(info, otherInfo, level, otherLevel) && + TextureCompatibility.CopySizeMatches(info, otherInfo, level, otherLevel)) + { + // These textures are copy compatible. Create the dependency. + + EnsureFullSubdivision(); + other.EnsureFullSubdivision(); + + TextureGroupHandle handle = _handles[i]; + TextureGroupHandle otherHandle = other._handles[j]; + + handle.CreateCopyDependency(otherHandle, copyTo); + + // If "copyTo" is true, this texture must copy to the other. + // Otherwise, it must copy to this texture. + + if (copyTo) + { + otherHandle.Copy(_context, handle); + } + else + { + handle.Copy(_context, otherHandle); + } + } + } + } + } + } + + /// <summary> + /// Registers another texture group as an incompatible overlap, if not already registered. + /// </summary> + /// <param name="other">The texture group to add to the incompatible overlaps list</param> + /// <param name="copy">True if the overlap should register copy dependencies</param> + public void RegisterIncompatibleOverlap(TextureIncompatibleOverlap other, bool copy) + { + if (!_incompatibleOverlaps.Exists(overlap => overlap.Group == other.Group)) + { + if (copy && other.Compatibility == TextureViewCompatibility.LayoutIncompatible) + { + // Any of the group's views may share compatibility, even if the parents do not fully. + CreateCopyDependency(other.Group, false); + } + + _incompatibleOverlaps.Add(other); + other.Group._incompatibleOverlaps.Add(new TextureIncompatibleOverlap(this, other.Compatibility)); + } + + other.Group.SignalIncompatibleOverlapModified(); + SignalIncompatibleOverlapModified(); + } + + /// <summary> + /// Clear modified flags in the given range. + /// This will stop any GPU written data from flushing or copying to dependent textures. + /// </summary> + /// <param name="range">The range to clear modified flags in</param> + /// <param name="ignore">Ignore handles that have a copy dependency to the specified group</param> + public void ClearModified(MultiRange range, TextureGroup ignore = null) + { + TextureGroupHandle[] handles = _handles; + + foreach (TextureGroupHandle handle in handles) + { + // Handles list is not modified by another thread, only replaced, so this is thread safe. + // Remove modified flags from all overlapping handles, so that the textures don't flush to unmapped/remapped GPU memory. + + MultiRange subRange = Storage.Range.Slice((ulong)handle.Offset, (ulong)handle.Size); + + if (range.OverlapsWith(subRange)) + { + if ((ignore == null || !handle.HasDependencyTo(ignore)) && handle.Modified) + { + handle.Modified = false; + Storage.SignalModifiedDirty(); + + lock (handle.Overlaps) + { + foreach (Texture texture in handle.Overlaps) + { + texture.SignalModifiedDirty(); + } + } + } + } + } + + Storage.SignalModifiedDirty(); + + if (_views != null) + { + foreach (Texture texture in _views) + { + texture.SignalModifiedDirty(); + } + } + } + + /// <summary> + /// A flush has been requested on a tracked region. Flush texture data for the given handle. + /// </summary> + /// <param name="handle">The handle this flush action is for</param> + /// <param name="address">The address of the flushing memory access</param> + /// <param name="size">The size of the flushing memory access</param> + public void FlushAction(TextureGroupHandle handle, ulong address, ulong size) + { + // If the page size is larger than 4KB, we will have a lot of false positives for flushing. + // Let's avoid flushing textures that are unlikely to be read from CPU to improve performance + // on those platforms. + if (!_physicalMemory.Supports4KBPages && !Storage.Info.IsLinear && !_context.IsGpuThread()) + { + return; + } + + // There is a small gap here where the action is removed but _actionRegistered is still 1. + // In this case it will skip registering the action, but here we are already handling it, + // so there shouldn't be any issue as it's the same handler for all actions. + + handle.ClearActionRegistered(); + + if (!handle.Modified) + { + return; + } + + bool isGpuThread = _context.IsGpuThread(); + + if (isGpuThread) + { + // No need to wait if we're on the GPU thread, we can just clear the modified flag immediately. + handle.Modified = false; + } + + _context.Renderer.BackgroundContextAction(() => + { + if (!isGpuThread) + { + handle.Sync(_context); + } + + Storage.SignalModifiedDirty(); + + lock (handle.Overlaps) + { + foreach (Texture texture in handle.Overlaps) + { + texture.SignalModifiedDirty(); + } + } + + if (TextureCompatibility.CanTextureFlush(Storage.Info, _context.Capabilities)) + { + FlushSliceRange(false, handle.BaseSlice, handle.BaseSlice + handle.SliceCount, Storage.GetFlushTexture()); + } + }); + } + + /// <summary> + /// Dispose this texture group, disposing all related memory tracking handles. + /// </summary> + public void Dispose() + { + foreach (TextureGroupHandle group in _handles) + { + group.Dispose(); + } + + foreach (TextureIncompatibleOverlap incompatible in _incompatibleOverlaps) + { + incompatible.Group._incompatibleOverlaps.RemoveAll(overlap => overlap.Group == this); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs new file mode 100644 index 00000000..ebb4e9ae --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -0,0 +1,554 @@ +using Ryujinx.Cpu.Tracking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// A tracking handle for a texture group, which represents a range of views in a storage texture. + /// Retains a list of overlapping texture views, a modified flag, and tracking for each + /// CPU VA range that the views cover. + /// Also tracks copy dependencies for the handle - references to other handles that must be kept + /// in sync with this one before use. + /// </summary> + class TextureGroupHandle : IDisposable + { + private TextureGroup _group; + private int _bindCount; + private int _firstLevel; + private int _firstLayer; + + // Sync state for texture flush. + + /// <summary> + /// The sync number last registered. + /// </summary> + private ulong _registeredSync; + + /// <summary> + /// The sync number when the texture was last modified by GPU. + /// </summary> + private ulong _modifiedSync; + + /// <summary> + /// Whether a tracking action is currently registered or not. (0/1) + /// </summary> + private int _actionRegistered; + + /// <summary> + /// Whether a sync action is currently registered or not. + /// </summary> + private bool _syncActionRegistered; + + /// <summary> + /// The byte offset from the start of the storage of this handle. + /// </summary> + public int Offset { get; } + + /// <summary> + /// The size in bytes covered by this handle. + /// </summary> + public int Size { get; } + + /// <summary> + /// The base slice index for this handle. + /// </summary> + public int BaseSlice { get; } + + /// <summary> + /// The number of slices covered by this handle. + /// </summary> + public int SliceCount { get; } + + /// <summary> + /// The textures which this handle overlaps with. + /// </summary> + public List<Texture> Overlaps { get; } + + /// <summary> + /// The CPU memory tracking handles that cover this handle. + /// </summary> + public CpuRegionHandle[] Handles { get; } + + /// <summary> + /// True if a texture overlapping this handle has been modified. Is set false when the flush action is called. + /// </summary> + public bool Modified { get; set; } + + /// <summary> + /// Dependencies to handles from other texture groups. + /// </summary> + public List<TextureDependency> Dependencies { get; } + + /// <summary> + /// A flag indicating that a copy is required from one of the dependencies. + /// </summary> + public bool NeedsCopy => DeferredCopy != null; + + /// <summary> + /// A data copy that must be acknowledged the next time this handle is used. + /// </summary> + public TextureGroupHandle DeferredCopy { get; set; } + + /// <summary> + /// Create a new texture group handle, representing a range of views in a storage texture. + /// </summary> + /// <param name="group">The TextureGroup that the handle belongs to</param> + /// <param name="offset">The byte offset from the start of the storage of the handle</param> + /// <param name="size">The size in bytes covered by the handle</param> + /// <param name="views">All views of the storage texture, used to calculate overlaps</param> + /// <param name="firstLayer">The first layer of this handle in the storage texture</param> + /// <param name="firstLevel">The first level of this handle in the storage texture</param> + /// <param name="baseSlice">The base slice index of this handle</param> + /// <param name="sliceCount">The number of slices this handle covers</param> + /// <param name="handles">The memory tracking handles that cover this handle</param> + public TextureGroupHandle(TextureGroup group, + int offset, + ulong size, + List<Texture> views, + int firstLayer, + int firstLevel, + int baseSlice, + int sliceCount, + CpuRegionHandle[] handles) + { + _group = group; + _firstLayer = firstLayer; + _firstLevel = firstLevel; + + Offset = offset; + Size = (int)size; + Overlaps = new List<Texture>(); + Dependencies = new List<TextureDependency>(); + + BaseSlice = baseSlice; + SliceCount = sliceCount; + + if (views != null) + { + RecalculateOverlaps(group, views); + } + + Handles = handles; + } + + /// <summary> + /// Calculate a list of which views overlap this handle. + /// </summary> + /// <param name="group">The parent texture group, used to find a view's base CPU VA offset</param> + /// <param name="views">The list of views to search for overlaps</param> + public void RecalculateOverlaps(TextureGroup group, List<Texture> views) + { + // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic. + lock (Overlaps) + { + int endOffset = Offset + Size; + + Overlaps.Clear(); + + foreach (Texture view in views) + { + int viewOffset = group.FindOffset(view); + if (viewOffset < endOffset && Offset < viewOffset + (int)view.Size) + { + Overlaps.Add(view); + } + } + } + } + + /// <summary> + /// Adds a single texture view as an overlap if its range overlaps. + /// </summary> + /// <param name="offset">The offset of the view in the group</param> + /// <param name="view">The texture to add as an overlap</param> + public void AddOverlap(int offset, Texture view) + { + // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic. + + if (OverlapsWith(offset, (int)view.Size)) + { + lock (Overlaps) + { + Overlaps.Add(view); + } + } + } + + /// <summary> + /// Removes a single texture view as an overlap if its range overlaps. + /// </summary> + /// <param name="offset">The offset of the view in the group</param> + /// <param name="view">The texture to add as an overlap</param> + public void RemoveOverlap(int offset, Texture view) + { + // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic. + + if (OverlapsWith(offset, (int)view.Size)) + { + lock (Overlaps) + { + Overlaps.Remove(view); + } + } + } + + /// <summary> + /// Registers a sync action to happen for this handle, and an interim flush action on the tracking handle. + /// </summary> + /// <param name="context">The GPU context to register a sync action on</param> + private void RegisterSync(GpuContext context) + { + if (!_syncActionRegistered) + { + _modifiedSync = context.SyncNumber; + context.RegisterSyncAction(SyncAction, true); + _syncActionRegistered = true; + } + + if (Interlocked.Exchange(ref _actionRegistered, 1) == 0) + { + _group.RegisterAction(this); + } + } + + /// <summary> + /// Signal that this handle has been modified to any existing dependencies, and set the modified flag. + /// </summary> + /// <param name="context">The GPU context to register a sync action on</param> + public void SignalModified(GpuContext context) + { + Modified = true; + + // If this handle has any copy dependencies, notify the other handle that a copy needs to be performed. + + foreach (TextureDependency dependency in Dependencies) + { + dependency.SignalModified(); + } + + RegisterSync(context); + } + + /// <summary> + /// Signal that this handle has either started or ended being modified. + /// </summary> + /// <param name="bound">True if this handle is being bound, false if unbound</param> + /// <param name="context">The GPU context to register a sync action on</param> + public void SignalModifying(bool bound, GpuContext context) + { + SignalModified(context); + + // Note: Bind count currently resets to 0 on inherit for safety, as the handle <-> view relationship can change. + _bindCount = Math.Max(0, _bindCount + (bound ? 1 : -1)); + } + + /// <summary> + /// Synchronize dependent textures, if any of them have deferred a copy from this texture. + /// </summary> + public void SynchronizeDependents() + { + foreach (TextureDependency dependency in Dependencies) + { + TextureGroupHandle otherHandle = dependency.Other.Handle; + + if (otherHandle.DeferredCopy == this) + { + otherHandle._group.Storage.SynchronizeMemory(); + } + } + } + + /// <summary> + /// Wait for the latest sync number that the texture handle was written to, + /// removing the modified flag if it was reached, or leaving it set if it has not yet been created. + /// </summary> + /// <param name="context">The GPU context used to wait for sync</param> + public void Sync(GpuContext context) + { + ulong registeredSync = _registeredSync; + long diff = (long)(context.SyncNumber - registeredSync); + + if (diff > 0) + { + context.Renderer.WaitSync(registeredSync); + + if ((long)(_modifiedSync - registeredSync) > 0) + { + // Flush the data in a previous state. Do not remove the modified flag - it will be removed to ignore following writes. + return; + } + + Modified = false; + } + + // If the difference is <= 0, no data is not ready yet. Flush any data we can without waiting or removing modified flag. + } + + /// <summary> + /// Clears the action registered variable, indicating that the tracking action should be + /// re-registered on the next modification. + /// </summary> + public void ClearActionRegistered() + { + Interlocked.Exchange(ref _actionRegistered, 0); + } + + /// <summary> + /// Action to perform when a sync number is registered after modification. + /// This action will register a read tracking action on the memory tracking handle so that a flush from CPU can happen. + /// </summary> + private void SyncAction() + { + // The storage will need to signal modified again to update the sync number in future. + _group.Storage.SignalModifiedDirty(); + + lock (Overlaps) + { + foreach (Texture texture in Overlaps) + { + texture.SignalModifiedDirty(); + } + } + + // Register region tracking for CPU? (again) + _registeredSync = _modifiedSync; + _syncActionRegistered = false; + + if (Interlocked.Exchange(ref _actionRegistered, 1) == 0) + { + _group.RegisterAction(this); + } + } + + /// <summary> + /// Signal that a copy dependent texture has been modified, and must have its data copied to this one. + /// </summary> + /// <param name="copyFrom">The texture handle that must defer a copy to this one</param> + public void DeferCopy(TextureGroupHandle copyFrom) + { + Modified = false; + + DeferredCopy = copyFrom; + + _group.Storage.SignalGroupDirty(); + + foreach (Texture overlap in Overlaps) + { + overlap.SignalGroupDirty(); + } + } + + /// <summary> + /// Create a copy dependency between this handle, and another. + /// </summary> + /// <param name="other">The handle to create a copy dependency to</param> + /// <param name="copyToOther">True if a copy should be deferred to all of the other handle's dependencies</param> + public void CreateCopyDependency(TextureGroupHandle other, bool copyToOther = false) + { + // Does this dependency already exist? + foreach (TextureDependency existing in Dependencies) + { + if (existing.Other.Handle == other) + { + // Do not need to create it again. May need to set the dirty flag. + return; + } + } + + _group.HasCopyDependencies = true; + other._group.HasCopyDependencies = true; + + TextureDependency dependency = new TextureDependency(this); + TextureDependency otherDependency = new TextureDependency(other); + + dependency.Other = otherDependency; + otherDependency.Other = dependency; + + Dependencies.Add(dependency); + other.Dependencies.Add(otherDependency); + + // Recursively create dependency: + // All of this handle's dependencies must depend on the other. + foreach (TextureDependency existing in Dependencies.ToArray()) + { + if (existing != dependency && existing.Other.Handle != other) + { + existing.Other.Handle.CreateCopyDependency(other); + } + } + + // All of the other handle's dependencies must depend on this. + foreach (TextureDependency existing in other.Dependencies.ToArray()) + { + if (existing != otherDependency && existing.Other.Handle != this) + { + existing.Other.Handle.CreateCopyDependency(this); + + if (copyToOther) + { + existing.Other.Handle.DeferCopy(this); + } + } + } + } + + /// <summary> + /// Remove a dependency from this handle's dependency list. + /// </summary> + /// <param name="dependency">The dependency to remove</param> + public void RemoveDependency(TextureDependency dependency) + { + Dependencies.Remove(dependency); + } + + /// <summary> + /// Check if any of this handle's memory tracking handles are dirty. + /// </summary> + /// <returns>True if at least one of the handles is dirty</returns> + private bool CheckDirty() + { + return Handles.Any(handle => handle.Dirty); + } + + /// <summary> + /// Perform a copy from the provided handle to this one, or perform a deferred copy if none is provided. + /// </summary> + /// <param name="context">GPU context to register sync for modified handles</param> + /// <param name="fromHandle">The handle to copy from. If not provided, this method will copy from and clear the deferred copy instead</param> + /// <returns>True if the copy was performed, false otherwise</returns> + public bool Copy(GpuContext context, TextureGroupHandle fromHandle = null) + { + bool result = false; + bool shouldCopy = false; + + if (fromHandle == null) + { + fromHandle = DeferredCopy; + + if (fromHandle != null) + { + // Only copy if the copy texture is still modified. + // It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush. + // It will also set as unmodified if a copy is deferred to it. + + shouldCopy = fromHandle.Modified; + + if (fromHandle._bindCount == 0) + { + // Repeat the copy in future if the bind count is greater than 0. + DeferredCopy = null; + } + } + } + else + { + // Copies happen directly when initializing a copy dependency. + // If dirty, do not copy. Its data no longer matters, and this handle should also be dirty. + // Also, only direct copy if the data in this handle is not already modified (can be set by copies from modified handles). + shouldCopy = !fromHandle.CheckDirty() && (fromHandle.Modified || !Modified); + } + + if (shouldCopy) + { + Texture from = fromHandle._group.Storage; + Texture to = _group.Storage; + + if (from.ScaleFactor != to.ScaleFactor) + { + to.PropagateScale(from); + } + + from.HostTexture.CopyTo( + to.HostTexture, + fromHandle._firstLayer, + _firstLayer, + fromHandle._firstLevel, + _firstLevel); + + if (fromHandle.Modified) + { + Modified = true; + + RegisterSync(context); + } + + result = true; + } + + return result; + } + + /// <summary> + /// Check if this handle has a dependency to a given texture group. + /// </summary> + /// <param name="group">The texture group to check for</param> + /// <returns>True if there is a dependency, false otherwise</returns> + public bool HasDependencyTo(TextureGroup group) + { + foreach (TextureDependency dep in Dependencies) + { + if (dep.Other.Handle._group == group) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Inherit modified flags and dependencies from another texture handle. + /// </summary> + /// <param name="old">The texture handle to inherit from</param> + /// <param name="withCopies">Whether the handle should inherit copy dependencies or not</param> + public void Inherit(TextureGroupHandle old, bool withCopies) + { + Modified |= old.Modified; + + if (withCopies) + { + foreach (TextureDependency dependency in old.Dependencies.ToArray()) + { + CreateCopyDependency(dependency.Other.Handle); + + if (dependency.Other.Handle.DeferredCopy == old) + { + dependency.Other.Handle.DeferredCopy = this; + } + } + + DeferredCopy = old.DeferredCopy; + } + } + + /// <summary> + /// Check if this region overlaps with another. + /// </summary> + /// <param name="address">Base address</param> + /// <param name="size">Size of the region</param> + /// <returns>True if overlapping, false otherwise</returns> + public bool OverlapsWith(int offset, int size) + { + return Offset < offset + size && offset < Offset + Size; + } + + /// <summary> + /// Dispose this texture group handle, removing all its dependencies and disposing its memory tracking handles. + /// </summary> + public void Dispose() + { + foreach (CpuRegionHandle handle in Handles) + { + handle.Dispose(); + } + + foreach (TextureDependency dependency in Dependencies.ToArray()) + { + dependency.Other.Handle.RemoveDependency(dependency.Other); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs new file mode 100644 index 00000000..a7ee12bc --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs @@ -0,0 +1,381 @@ +using Ryujinx.Common; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Texture; +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture information. + /// </summary> + readonly struct TextureInfo + { + /// <summary> + /// Address of the texture in GPU mapped memory. + /// </summary> + public ulong GpuAddress { get; } + + /// <summary> + /// The width of the texture. + /// </summary> + public int Width { get; } + + /// <summary> + /// The height of the texture, or layers count for 1D array textures. + /// </summary> + public int Height { get; } + + /// <summary> + /// The depth of the texture (for 3D textures), or layers count for array textures. + /// </summary> + public int DepthOrLayers { get; } + + /// <summary> + /// The number of mipmap levels of the texture. + /// </summary> + public int Levels { get; } + + /// <summary> + /// The number of samples in the X direction for multisampled textures. + /// </summary> + public int SamplesInX { get; } + + /// <summary> + /// The number of samples in the Y direction for multisampled textures. + /// </summary> + public int SamplesInY { get; } + + /// <summary> + /// The number of bytes per line for linear textures. + /// </summary> + public int Stride { get; } + + /// <summary> + /// Indicates whenever or not the texture is a linear texture. + /// </summary> + public bool IsLinear { get; } + + /// <summary> + /// GOB blocks in the Y direction, for block linear textures. + /// </summary> + public int GobBlocksInY { get; } + + /// <summary> + /// GOB blocks in the Z direction, for block linear textures. + /// </summary> + public int GobBlocksInZ { get; } + + /// <summary> + /// Number of GOB blocks per tile in the X direction, for block linear textures. + /// </summary> + public int GobBlocksInTileX { get; } + + /// <summary> + /// Total number of samples for multisampled textures. + /// </summary> + public int Samples => SamplesInX * SamplesInY; + + /// <summary> + /// Texture target type. + /// </summary> + public Target Target { get; } + + /// <summary> + /// Texture format information. + /// </summary> + public FormatInfo FormatInfo { get; } + + /// <summary> + /// Depth-stencil mode of the texture. This defines whenever the depth or stencil value is read from shaders, + /// for depth-stencil texture formats. + /// </summary> + public DepthStencilMode DepthStencilMode { get; } + + /// <summary> + /// Texture swizzle for the red color channel. + /// </summary> + public SwizzleComponent SwizzleR { get; } + + /// <summary> + /// Texture swizzle for the green color channel. + /// </summary> + public SwizzleComponent SwizzleG { get; } + + /// <summary> + /// Texture swizzle for the blue color channel. + /// </summary> + public SwizzleComponent SwizzleB { get; } + + /// <summary> + /// Texture swizzle for the alpha color channel. + /// </summary> + public SwizzleComponent SwizzleA { get; } + + /// <summary> + /// Constructs the texture information structure. + /// </summary> + /// <param name="gpuAddress">The GPU address of the texture</param> + /// <param name="width">The width of the texture</param> + /// <param name="height">The height or the texture</param> + /// <param name="depthOrLayers">The depth or layers count of the texture</param> + /// <param name="levels">The amount of mipmap levels of the texture</param> + /// <param name="samplesInX">The number of samples in the X direction for multisample textures, should be 1 otherwise</param> + /// <param name="samplesInY">The number of samples in the Y direction for multisample textures, should be 1 otherwise</param> + /// <param name="stride">The stride for linear textures</param> + /// <param name="isLinear">Whenever the texture is linear or block linear</param> + /// <param name="gobBlocksInY">Number of GOB blocks in the Y direction</param> + /// <param name="gobBlocksInZ">Number of GOB blocks in the Z direction</param> + /// <param name="gobBlocksInTileX">Number of GOB blocks per tile in the X direction</param> + /// <param name="target">Texture target type</param> + /// <param name="formatInfo">Texture format information</param> + /// <param name="depthStencilMode">Depth-stencil mode</param> + /// <param name="swizzleR">Swizzle for the red color channel</param> + /// <param name="swizzleG">Swizzle for the green color channel</param> + /// <param name="swizzleB">Swizzle for the blue color channel</param> + /// <param name="swizzleA">Swizzle for the alpha color channel</param> + public TextureInfo( + ulong gpuAddress, + int width, + int height, + int depthOrLayers, + int levels, + int samplesInX, + int samplesInY, + int stride, + bool isLinear, + int gobBlocksInY, + int gobBlocksInZ, + int gobBlocksInTileX, + Target target, + FormatInfo formatInfo, + DepthStencilMode depthStencilMode = DepthStencilMode.Depth, + SwizzleComponent swizzleR = SwizzleComponent.Red, + SwizzleComponent swizzleG = SwizzleComponent.Green, + SwizzleComponent swizzleB = SwizzleComponent.Blue, + SwizzleComponent swizzleA = SwizzleComponent.Alpha) + { + GpuAddress = gpuAddress; + Width = width; + Height = height; + DepthOrLayers = depthOrLayers; + Levels = levels; + SamplesInX = samplesInX; + SamplesInY = samplesInY; + Stride = stride; + IsLinear = isLinear; + GobBlocksInY = gobBlocksInY; + GobBlocksInZ = gobBlocksInZ; + GobBlocksInTileX = gobBlocksInTileX; + Target = target; + FormatInfo = formatInfo; + DepthStencilMode = depthStencilMode; + SwizzleR = swizzleR; + SwizzleG = swizzleG; + SwizzleB = swizzleB; + SwizzleA = swizzleA; + } + + /// <summary> + /// Gets the real texture depth. + /// Returns 1 for any target other than 3D textures. + /// </summary> + /// <returns>Texture depth</returns> + public int GetDepth() + { + return GetDepth(Target, DepthOrLayers); + } + + /// <summary> + /// Gets the real texture depth. + /// Returns 1 for any target other than 3D textures. + /// </summary> + /// <param name="target">Texture target</param> + /// <param name="depthOrLayers">Texture depth if the texture is 3D, otherwise ignored</param> + /// <returns>Texture depth</returns> + public static int GetDepth(Target target, int depthOrLayers) + { + return target == Target.Texture3D ? depthOrLayers : 1; + } + + /// <summary> + /// Gets the number of layers of the texture. + /// Returns 1 for non-array textures, 6 for cubemap textures, and layer faces for cubemap array textures. + /// </summary> + /// <returns>The number of texture layers</returns> + public int GetLayers() + { + return GetLayers(Target, DepthOrLayers); + } + + /// <summary> + /// Gets the number of layers of the texture. + /// Returns 1 for non-array textures, 6 for cubemap textures, and layer faces for cubemap array textures. + /// </summary> + /// <param name="target">Texture target</param> + /// <param name="depthOrLayers">Texture layers if the is a array texture, ignored otherwise</param> + /// <returns>The number of texture layers</returns> + public static int GetLayers(Target target, int depthOrLayers) + { + if (target == Target.Texture2DArray || target == Target.Texture2DMultisampleArray) + { + return depthOrLayers; + } + else if (target == Target.CubemapArray) + { + return depthOrLayers * 6; + } + else if (target == Target.Cubemap) + { + return 6; + } + else + { + return 1; + } + } + + /// <summary> + /// Gets the number of 2D slices of the texture. + /// Returns 6 for cubemap textures, layer faces for cubemap array textures, and DepthOrLayers for everything else. + /// </summary> + /// <returns>The number of texture slices</returns> + public int GetSlices() + { + if (Target == Target.Texture3D || Target == Target.Texture2DArray || Target == Target.Texture2DMultisampleArray) + { + return DepthOrLayers; + } + else if (Target == Target.CubemapArray) + { + return DepthOrLayers * 6; + } + else if (Target == Target.Cubemap) + { + return 6; + } + else + { + return 1; + } + } + + /// <summary> + /// Calculates the size information from the texture information. + /// </summary> + /// <param name="layerSize">Optional size of each texture layer in bytes</param> + /// <returns>Texture size information</returns> + public SizeInfo CalculateSizeInfo(int layerSize = 0) + { + if (Target == Target.TextureBuffer) + { + return new SizeInfo(Width * FormatInfo.BytesPerPixel); + } + else if (IsLinear) + { + return SizeCalculator.GetLinearTextureSize( + Stride, + Height, + FormatInfo.BlockHeight); + } + else + { + return SizeCalculator.GetBlockLinearTextureSize( + Width, + Height, + GetDepth(), + Levels, + GetLayers(), + FormatInfo.BlockWidth, + FormatInfo.BlockHeight, + FormatInfo.BytesPerPixel, + GobBlocksInY, + GobBlocksInZ, + GobBlocksInTileX, + layerSize); + } + } + + /// <summary> + /// Creates texture information for a given mipmap level of the specified parent texture and this information. + /// </summary> + /// <param name="parent">The parent texture</param> + /// <param name="firstLevel">The first level of the texture view</param> + /// <returns>The adjusted texture information with the new size</returns> + public TextureInfo CreateInfoForLevelView(Texture parent, int firstLevel) + { + // When the texture is used as view of another texture, we must + // ensure that the sizes are valid, otherwise data uploads would fail + // (and the size wouldn't match the real size used on the host API). + // Given a parent texture from where the view is created, we have the + // following rules: + // - The view size must be equal to the parent size, divided by (2 ^ l), + // where l is the first mipmap level of the view. The division result must + // be rounded down, and the result must be clamped to 1. + // - If the parent format is compressed, and the view format isn't, the + // view size is calculated as above, but the width and height of the + // view must be also divided by the compressed format block width and height. + // - If the parent format is not compressed, and the view is, the view + // size is calculated as described on the first point, but the width and height + // of the view must be also multiplied by the block width and height. + int width = Math.Max(1, parent.Info.Width >> firstLevel); + int height = Math.Max(1, parent.Info.Height >> firstLevel); + + if (parent.Info.FormatInfo.IsCompressed && !FormatInfo.IsCompressed) + { + width = BitUtils.DivRoundUp(width, parent.Info.FormatInfo.BlockWidth); + height = BitUtils.DivRoundUp(height, parent.Info.FormatInfo.BlockHeight); + } + else if (!parent.Info.FormatInfo.IsCompressed && FormatInfo.IsCompressed) + { + width *= FormatInfo.BlockWidth; + height *= FormatInfo.BlockHeight; + } + + int depthOrLayers; + + if (Target == Target.Texture3D) + { + depthOrLayers = Math.Max(1, parent.Info.DepthOrLayers >> firstLevel); + } + else + { + depthOrLayers = DepthOrLayers; + } + + // 2D and 2D multisample textures are not considered compatible. + // This specific case is required for copies, where the source texture might be multisample. + // In this case, we inherit the parent texture multisample state. + Target target = Target; + int samplesInX = SamplesInX; + int samplesInY = SamplesInY; + + if (target == Target.Texture2D && parent.Target == Target.Texture2DMultisample) + { + target = Target.Texture2DMultisample; + samplesInX = parent.Info.SamplesInX; + samplesInY = parent.Info.SamplesInY; + } + + return new TextureInfo( + GpuAddress, + width, + height, + depthOrLayers, + Levels, + samplesInX, + samplesInY, + Stride, + IsLinear, + GobBlocksInY, + GobBlocksInZ, + GobBlocksInTileX, + target, + FormatInfo, + DepthStencilMode, + SwizzleR, + SwizzleG, + SwizzleB, + SwizzleA); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs new file mode 100644 index 00000000..266f6285 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -0,0 +1,498 @@ +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Engine.Types; +using Ryujinx.Graphics.Gpu.Shader; +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture manager. + /// </summary> + class TextureManager : IDisposable + { + private readonly GpuContext _context; + private readonly GpuChannel _channel; + + private readonly TextureBindingsManager _cpBindingsManager; + private readonly TextureBindingsManager _gpBindingsManager; + private readonly TexturePoolCache _texturePoolCache; + private readonly SamplerPoolCache _samplerPoolCache; + + private readonly Texture[] _rtColors; + private readonly ITexture[] _rtHostColors; + private Texture _rtDepthStencil; + private ITexture _rtHostDs; + + public int ClipRegionWidth { get; private set; } + public int ClipRegionHeight { get; private set; } + + /// <summary> + /// The scaling factor applied to all currently bound render targets. + /// </summary> + public float RenderTargetScale { get; private set; } = 1f; + + /// <summary> + /// Creates a new instance of the texture manager. + /// </summary> + /// <param name="context">GPU context that the texture manager belongs to</param> + /// <param name="channel">GPU channel that the texture manager belongs to</param> + public TextureManager(GpuContext context, GpuChannel channel) + { + _context = context; + _channel = channel; + + TexturePoolCache texturePoolCache = new TexturePoolCache(context); + SamplerPoolCache samplerPoolCache = new SamplerPoolCache(context); + + float[] scales = new float[64]; + new Span<float>(scales).Fill(1f); + + _cpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: true); + _gpBindingsManager = new TextureBindingsManager(context, channel, texturePoolCache, samplerPoolCache, scales, isCompute: false); + _texturePoolCache = texturePoolCache; + _samplerPoolCache = samplerPoolCache; + + _rtColors = new Texture[Constants.TotalRenderTargets]; + _rtHostColors = new ITexture[Constants.TotalRenderTargets]; + } + + /// <summary> + /// Sets the texture and image bindings for the compute pipeline. + /// </summary> + /// <param name="bindings">Bindings for the active shader</param> + public void SetComputeBindings(CachedShaderBindings bindings) + { + _cpBindingsManager.SetBindings(bindings); + } + + /// <summary> + /// Sets the texture and image bindings for the graphics pipeline. + /// </summary> + /// <param name="bindings">Bindings for the active shader</param> + public void SetGraphicsBindings(CachedShaderBindings bindings) + { + _gpBindingsManager.SetBindings(bindings); + } + + /// <summary> + /// Sets the texture constant buffer index on the compute pipeline. + /// </summary> + /// <param name="index">The texture constant buffer index</param> + public void SetComputeTextureBufferIndex(int index) + { + _cpBindingsManager.SetTextureBufferIndex(index); + } + + /// <summary> + /// Sets the texture constant buffer index on the graphics pipeline. + /// </summary> + /// <param name="index">The texture constant buffer index</param> + public void SetGraphicsTextureBufferIndex(int index) + { + _gpBindingsManager.SetTextureBufferIndex(index); + } + + /// <summary> + /// Sets the current sampler pool on the compute pipeline. + /// </summary> + /// <param name="gpuVa">The start GPU virtual address of the sampler pool</param> + /// <param name="maximumId">The maximum ID of the sampler pool</param> + /// <param name="samplerIndex">The indexing type of the sampler pool</param> + public void SetComputeSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex) + { + _cpBindingsManager.SetSamplerPool(gpuVa, maximumId, samplerIndex); + } + + /// <summary> + /// Sets the current sampler pool on the graphics pipeline. + /// </summary> + /// <param name="gpuVa">The start GPU virtual address of the sampler pool</param> + /// <param name="maximumId">The maximum ID of the sampler pool</param> + /// <param name="samplerIndex">The indexing type of the sampler pool</param> + public void SetGraphicsSamplerPool(ulong gpuVa, int maximumId, SamplerIndex samplerIndex) + { + _gpBindingsManager.SetSamplerPool(gpuVa, maximumId, samplerIndex); + } + + /// <summary> + /// Sets the current texture pool on the compute pipeline. + /// </summary> + /// <param name="gpuVa">The start GPU virtual address of the texture pool</param> + /// <param name="maximumId">The maximum ID of the texture pool</param> + public void SetComputeTexturePool(ulong gpuVa, int maximumId) + { + _cpBindingsManager.SetTexturePool(gpuVa, maximumId); + } + + /// <summary> + /// Sets the current texture pool on the graphics pipeline. + /// </summary> + /// <param name="gpuVa">The start GPU virtual address of the texture pool</param> + /// <param name="maximumId">The maximum ID of the texture pool</param> + public void SetGraphicsTexturePool(ulong gpuVa, int maximumId) + { + _gpBindingsManager.SetTexturePool(gpuVa, maximumId); + } + + /// <summary> + /// Check if a texture's scale must be updated to match the configured resolution scale. + /// </summary> + /// <param name="texture">The texture to check</param> + /// <returns>True if the scale needs updating, false if the scale is up to date</returns> + private bool ScaleNeedsUpdated(Texture texture) + { + return texture != null && !(texture.ScaleMode == TextureScaleMode.Blacklisted || texture.ScaleMode == TextureScaleMode.Undesired) && texture.ScaleFactor != GraphicsConfig.ResScale; + } + + /// <summary> + /// Sets the render target color buffer. + /// </summary> + /// <param name="index">The index of the color buffer to set (up to 8)</param> + /// <param name="color">The color buffer texture</param> + /// <returns>True if render target scale must be updated.</returns> + public bool SetRenderTargetColor(int index, Texture color) + { + bool hasValue = color != null; + bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && RenderTargetScale != color.ScaleFactor); + + if (_rtColors[index] != color) + { + _rtColors[index]?.SignalModifying(false); + + if (color != null) + { + color.SynchronizeMemory(); + color.SignalModifying(true); + } + + _rtColors[index] = color; + } + + return changesScale || ScaleNeedsUpdated(color); + } + + /// <summary> + /// Sets the render target depth-stencil buffer. + /// </summary> + /// <param name="depthStencil">The depth-stencil buffer texture</param> + /// <returns>True if render target scale must be updated.</returns> + public bool SetRenderTargetDepthStencil(Texture depthStencil) + { + bool hasValue = depthStencil != null; + bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && RenderTargetScale != depthStencil.ScaleFactor); + + if (_rtDepthStencil != depthStencil) + { + _rtDepthStencil?.SignalModifying(false); + + if (depthStencil != null) + { + depthStencil.SynchronizeMemory(); + depthStencil.SignalModifying(true); + } + + _rtDepthStencil = depthStencil; + } + + return changesScale || ScaleNeedsUpdated(depthStencil); + } + + /// <summary> + /// Sets the host clip region, which should be the intersection of all render target texture sizes. + /// </summary> + /// <param name="width">Width of the clip region, defined as the minimum width across all bound textures</param> + /// <param name="height">Height of the clip region, defined as the minimum height across all bound textures</param> + public void SetClipRegion(int width, int height) + { + ClipRegionWidth = width; + ClipRegionHeight = height; + } + + /// <summary> + /// Gets the first available bound colour target, or the depth stencil target if not present. + /// </summary> + /// <returns>The first bound colour target, otherwise the depth stencil target</returns> + public Texture GetAnyRenderTarget() + { + return _rtColors[0] ?? _rtDepthStencil; + } + + /// <summary> + /// Updates the Render Target scale, given the currently bound render targets. + /// This will update scale to match the configured scale, scale textures that are eligible but not scaled, + /// and propagate blacklisted status from one texture to the ones bound with it. + /// </summary> + /// <param name="singleUse">If this is not -1, it indicates that only the given indexed target will be used.</param> + public void UpdateRenderTargetScale(int singleUse) + { + // Make sure all scales for render targets are at the highest they should be. Blacklisted targets should propagate their scale to the other targets. + bool mismatch = false; + bool blacklisted = false; + bool hasUpscaled = false; + bool hasUndesired = false; + float targetScale = GraphicsConfig.ResScale; + + void ConsiderTarget(Texture target) + { + if (target == null) return; + float scale = target.ScaleFactor; + + switch (target.ScaleMode) + { + case TextureScaleMode.Blacklisted: + mismatch |= scale != 1f; + blacklisted = true; + break; + case TextureScaleMode.Eligible: + mismatch = true; // We must make a decision. + break; + case TextureScaleMode.Undesired: + hasUndesired = true; + mismatch |= scale != 1f || hasUpscaled; // If another target is upscaled, scale this one up too. + break; + case TextureScaleMode.Scaled: + hasUpscaled = true; + mismatch |= hasUndesired || scale != targetScale; // If the target scale has changed, reset the scale for all targets. + break; + } + } + + if (singleUse != -1) + { + // If only one target is in use (by a clear, for example) the others do not need to be checked for mismatching scale. + ConsiderTarget(_rtColors[singleUse]); + } + else + { + foreach (Texture color in _rtColors) + { + ConsiderTarget(color); + } + } + + ConsiderTarget(_rtDepthStencil); + + mismatch |= blacklisted && hasUpscaled; + + if (blacklisted || (hasUndesired && !hasUpscaled)) + { + targetScale = 1f; + } + + if (mismatch) + { + if (blacklisted) + { + // Propagate the blacklisted state to the other textures. + foreach (Texture color in _rtColors) + { + color?.BlacklistScale(); + } + + _rtDepthStencil?.BlacklistScale(); + } + else + { + // Set the scale of the other textures. + foreach (Texture color in _rtColors) + { + color?.SetScale(targetScale); + } + + _rtDepthStencil?.SetScale(targetScale); + } + } + + RenderTargetScale = targetScale; + } + + /// <summary> + /// Gets a texture and a sampler from their respective pools from a texture ID and a sampler ID. + /// </summary> + /// <param name="textureId">ID of the texture</param> + /// <param name="samplerId">ID of the sampler</param> + public (Texture, Sampler) GetGraphicsTextureAndSampler(int textureId, int samplerId) + { + return _gpBindingsManager.GetTextureAndSampler(textureId, samplerId); + } + + /// <summary> + /// Commits bindings on the compute pipeline. + /// </summary> + /// <param name="specState">Specialization state for the bound shader</param> + /// <returns>True if all bound textures match the current shader specialization state, false otherwise</returns> + public bool CommitComputeBindings(ShaderSpecializationState specState) + { + // Every time we switch between graphics and compute work, + // we must rebind everything. + // Since compute work happens less often, we always do that + // before and after the compute dispatch. + + _texturePoolCache.Tick(); + _samplerPoolCache.Tick(); + + _cpBindingsManager.Rebind(); + bool result = _cpBindingsManager.CommitBindings(specState); + _gpBindingsManager.Rebind(); + + return result; + } + + /// <summary> + /// Commits bindings on the graphics pipeline. + /// </summary> + /// <param name="specState">Specialization state for the bound shader</param> + /// <returns>True if all bound textures match the current shader specialization state, false otherwise</returns> + public bool CommitGraphicsBindings(ShaderSpecializationState specState) + { + _texturePoolCache.Tick(); + _samplerPoolCache.Tick(); + + bool result = _gpBindingsManager.CommitBindings(specState); + + UpdateRenderTargets(); + + return result; + } + + /// <summary> + /// Returns a texture pool from the cache, with the given address and maximum id. + /// </summary> + /// <param name="poolGpuVa">GPU virtual address of the texture pool</param> + /// <param name="maximumId">Maximum ID of the texture pool</param> + /// <returns>The texture pool</returns> + public TexturePool GetTexturePool(ulong poolGpuVa, int maximumId) + { + ulong poolAddress = _channel.MemoryManager.Translate(poolGpuVa); + + TexturePool texturePool = _texturePoolCache.FindOrCreate(_channel, poolAddress, maximumId); + + return texturePool; + } + + /// <summary> + /// Gets a texture descriptor used on the compute pipeline. + /// </summary> + /// <param name="poolGpuVa">GPU virtual address of the texture pool</param> + /// <param name="bufferIndex">Index of the constant buffer with texture handles</param> + /// <param name="maximumId">Maximum ID of the texture pool</param> + /// <param name="handle">Shader "fake" handle of the texture</param> + /// <param name="cbufSlot">Shader constant buffer slot of the texture</param> + /// <returns>The texture descriptor</returns> + public TextureDescriptor GetComputeTextureDescriptor(ulong poolGpuVa, int bufferIndex, int maximumId, int handle, int cbufSlot) + { + return _cpBindingsManager.GetTextureDescriptor(poolGpuVa, bufferIndex, maximumId, 0, handle, cbufSlot); + } + + /// <summary> + /// Gets a texture descriptor used on the graphics pipeline. + /// </summary> + /// <param name="poolGpuVa">GPU virtual address of the texture pool</param> + /// <param name="bufferIndex">Index of the constant buffer with texture handles</param> + /// <param name="maximumId">Maximum ID of the texture pool</param> + /// <param name="stageIndex">Index of the shader stage where the texture is bound</param> + /// <param name="handle">Shader "fake" handle of the texture</param> + /// <param name="cbufSlot">Shader constant buffer slot of the texture</param> + /// <returns>The texture descriptor</returns> + public TextureDescriptor GetGraphicsTextureDescriptor( + ulong poolGpuVa, + int bufferIndex, + int maximumId, + int stageIndex, + int handle, + int cbufSlot) + { + return _gpBindingsManager.GetTextureDescriptor(poolGpuVa, bufferIndex, maximumId, stageIndex, handle, cbufSlot); + } + + /// <summary> + /// Update host framebuffer attachments based on currently bound render target buffers. + /// </summary> + public void UpdateRenderTargets() + { + bool anyChanged = false; + + if (_rtHostDs != _rtDepthStencil?.HostTexture) + { + _rtHostDs = _rtDepthStencil?.HostTexture; + + anyChanged = true; + } + + for (int index = 0; index < _rtColors.Length; index++) + { + ITexture hostTexture = _rtColors[index]?.HostTexture; + + if (_rtHostColors[index] != hostTexture) + { + _rtHostColors[index] = hostTexture; + + anyChanged = true; + } + } + + if (anyChanged) + { + _context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs); + } + } + + /// <summary> + /// Update host framebuffer attachments based on currently bound render target buffers. + /// </summary> + /// <remarks> + /// All color attachments will be unbound. + /// </remarks> + public void UpdateRenderTargetDepthStencil() + { + new Span<ITexture>(_rtHostColors).Fill(null); + _rtHostDs = _rtDepthStencil?.HostTexture; + + _context.Renderer.Pipeline.SetRenderTargets(_rtHostColors, _rtHostDs); + } + + /// <summary> + /// Forces the texture and sampler pools to be re-loaded from the cache on next use. + /// </summary> + public void ReloadPools() + { + _cpBindingsManager.ReloadPools(); + _gpBindingsManager.ReloadPools(); + } + + /// <summary> + /// Forces all textures, samplers, images and render targets to be rebound the next time + /// CommitGraphicsBindings is called. + /// </summary> + public void Rebind() + { + _gpBindingsManager.Rebind(); + + for (int index = 0; index < _rtHostColors.Length; index++) + { + _rtHostColors[index] = null; + } + + _rtHostDs = null; + } + + /// <summary> + /// Disposes the texture manager. + /// It's an error to use the texture manager after disposal. + /// </summary> + public void Dispose() + { + // Textures are owned by the texture cache, so we shouldn't dispose the texture pool cache. + _samplerPoolCache.Dispose(); + + for (int i = 0; i < _rtColors.Length; i++) + { + _rtColors[i]?.DecrementReferenceCount(); + _rtColors[i] = null; + } + + _rtDepthStencil?.DecrementReferenceCount(); + _rtDepthStencil = null; + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureMatchQuality.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureMatchQuality.cs new file mode 100644 index 00000000..1351bf24 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureMatchQuality.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + enum TextureMatchQuality + { + NoMatch, + FormatAlias, + Perfect + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureMsaaMode.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureMsaaMode.cs new file mode 100644 index 00000000..0461888f --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureMsaaMode.cs @@ -0,0 +1,68 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Multisampled texture samples count. + /// </summary> + enum TextureMsaaMode + { + Ms1x1 = 0, + Ms2x2 = 2, + Ms4x2 = 4, + Ms2x1 = 5, + Ms4x4 = 6 + } + + static class TextureMsaaModeConverter + { + /// <summary> + /// Returns the total number of samples from the MSAA mode. + /// </summary> + /// <param name="msaaMode">The MSAA mode</param> + /// <returns>The total number of samples</returns> + public static int SamplesCount(this TextureMsaaMode msaaMode) + { + return msaaMode switch + { + TextureMsaaMode.Ms2x1 => 2, + TextureMsaaMode.Ms2x2 => 4, + TextureMsaaMode.Ms4x2 => 8, + TextureMsaaMode.Ms4x4 => 16, + _ => 1 + }; + } + + /// <summary> + /// Returns the number of samples in the X direction from the MSAA mode. + /// </summary> + /// <param name="msaaMode">The MSAA mode</param> + /// <returns>The number of samples in the X direction</returns> + public static int SamplesInX(this TextureMsaaMode msaaMode) + { + return msaaMode switch + { + TextureMsaaMode.Ms2x1 => 2, + TextureMsaaMode.Ms2x2 => 2, + TextureMsaaMode.Ms4x2 => 4, + TextureMsaaMode.Ms4x4 => 4, + _ => 1 + }; + } + + /// <summary> + /// Returns the number of samples in the Y direction from the MSAA mode. + /// </summary> + /// <param name="msaaMode">The MSAA mode</param> + /// <returns>The number of samples in the Y direction</returns> + public static int SamplesInY(this TextureMsaaMode msaaMode) + { + return msaaMode switch + { + TextureMsaaMode.Ms2x1 => 1, + TextureMsaaMode.Ms2x2 => 2, + TextureMsaaMode.Ms4x2 => 2, + TextureMsaaMode.Ms4x4 => 4, + _ => 1 + }; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs new file mode 100644 index 00000000..5277e789 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs @@ -0,0 +1,603 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Texture; +using Ryujinx.Memory.Range; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture pool. + /// </summary> + class TexturePool : Pool<Texture, TextureDescriptor>, IPool<TexturePool> + { + /// <summary> + /// A request to dereference a texture from a pool. + /// </summary> + private struct DereferenceRequest + { + /// <summary> + /// Whether the dereference is due to a mapping change or not. + /// </summary> + public readonly bool IsRemapped; + + /// <summary> + /// The texture being dereferenced. + /// </summary> + public readonly Texture Texture; + + /// <summary> + /// The ID of the pool entry this reference belonged to. + /// </summary> + public readonly int ID; + + /// <summary> + /// Create a dereference request for a texture with a specific pool ID, and remapped flag. + /// </summary> + /// <param name="isRemapped">Whether the dereference is due to a mapping change or not</param> + /// <param name="texture">The texture being dereferenced</param> + /// <param name="id">The ID of the pool entry, used to restore remapped textures</param> + private DereferenceRequest(bool isRemapped, Texture texture, int id) + { + IsRemapped = isRemapped; + Texture = texture; + ID = id; + } + + /// <summary> + /// Create a dereference request for a texture removal. + /// </summary> + /// <param name="texture">The texture being removed</param> + /// <returns>A texture removal dereference request</returns> + public static DereferenceRequest Remove(Texture texture) + { + return new DereferenceRequest(false, texture, 0); + } + + /// <summary> + /// Create a dereference request for a texture remapping with a specific pool ID. + /// </summary> + /// <param name="texture">The texture being remapped</param> + /// <param name="id">The ID of the pool entry, used to restore remapped textures</param> + /// <returns>A remap dereference request</returns> + public static DereferenceRequest Remap(Texture texture, int id) + { + return new DereferenceRequest(true, texture, id); + } + } + + private readonly GpuChannel _channel; + private readonly ConcurrentQueue<DereferenceRequest> _dereferenceQueue = new ConcurrentQueue<DereferenceRequest>(); + private TextureDescriptor _defaultDescriptor; + + /// <summary> + /// Linked list node used on the texture pool cache. + /// </summary> + public LinkedListNode<TexturePool> CacheNode { get; set; } + + /// <summary> + /// Timestamp used by the texture pool cache, updated on every use of this texture pool. + /// </summary> + public ulong CacheTimestamp { get; set; } + + /// <summary> + /// Creates a new instance of the texture pool. + /// </summary> + /// <param name="context">GPU context that the texture pool belongs to</param> + /// <param name="channel">GPU channel that the texture pool belongs to</param> + /// <param name="address">Address of the texture pool in guest memory</param> + /// <param name="maximumId">Maximum texture ID of the texture pool (equal to maximum textures minus one)</param> + public TexturePool(GpuContext context, GpuChannel channel, ulong address, int maximumId) : base(context, channel.MemoryManager.Physical, address, maximumId) + { + _channel = channel; + } + + /// <summary> + /// Gets the texture descripor and texture with the given ID with no bounds check or synchronization. + /// </summary> + /// <param name="id">ID of the texture. This is effectively a zero-based index</param> + /// <param name="texture">The texture with the given ID</param> + /// <returns>The texture descriptor with the given ID</returns> + private ref readonly TextureDescriptor GetInternal(int id, out Texture texture) + { + texture = Items[id]; + + ref readonly TextureDescriptor descriptor = ref GetDescriptorRef(id); + + if (texture == null) + { + texture = PhysicalMemory.TextureCache.FindShortCache(descriptor); + + if (texture == null) + { + TextureInfo info = GetInfo(descriptor, out int layerSize); + + // The dereference queue can put our texture back on the cache. + if ((texture = ProcessDereferenceQueue(id)) != null) + { + return ref descriptor; + } + + texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize); + + // If this happens, then the texture address is invalid, we can't add it to the cache. + if (texture == null) + { + return ref descriptor; + } + } + else + { + texture.SynchronizeMemory(); + } + + Items[id] = texture; + + texture.IncrementReferenceCount(this, id, descriptor.UnpackAddress()); + + DescriptorCache[id] = descriptor; + } + else + { + // On the path above (texture not yet in the pool), memory is automatically synchronized on texture creation. + texture.SynchronizeMemory(); + } + + return ref descriptor; + } + + /// <summary> + /// Gets the texture with the given ID. + /// </summary> + /// <param name="id">ID of the texture. This is effectively a zero-based index</param> + /// <returns>The texture with the given ID</returns> + public override Texture Get(int id) + { + if ((uint)id >= Items.Length) + { + return null; + } + + if (SequenceNumber != Context.SequenceNumber) + { + SequenceNumber = Context.SequenceNumber; + + SynchronizeMemory(); + } + + GetInternal(id, out Texture texture); + + return texture; + } + + /// <summary> + /// Gets the texture descriptor and texture with the given ID. + /// </summary> + /// <remarks> + /// This method assumes that the pool has been manually synchronized before doing binding. + /// </remarks> + /// <param name="id">ID of the texture. This is effectively a zero-based index</param> + /// <param name="texture">The texture with the given ID</param> + /// <returns>The texture descriptor with the given ID</returns> + public ref readonly TextureDescriptor GetForBinding(int id, out Texture texture) + { + if ((uint)id >= Items.Length) + { + texture = null; + return ref _defaultDescriptor; + } + + // When getting for binding, assume the pool has already been synchronized. + + return ref GetInternal(id, out texture); + } + + /// <summary> + /// Checks if the pool was modified, and returns the last sequence number where a modification was detected. + /// </summary> + /// <returns>A number that increments each time a modification is detected</returns> + public int CheckModified() + { + if (SequenceNumber != Context.SequenceNumber) + { + SequenceNumber = Context.SequenceNumber; + + SynchronizeMemory(); + } + + return ModifiedSequenceNumber; + } + + /// <summary> + /// Forcibly remove a texture from this pool's items. + /// If deferred, the dereference will be queued to occur on the render thread. + /// </summary> + /// <param name="texture">The texture being removed</param> + /// <param name="id">The ID of the texture in this pool</param> + /// <param name="deferred">If true, queue the dereference to happen on the render thread, otherwise dereference immediately</param> + public void ForceRemove(Texture texture, int id, bool deferred) + { + var previous = Interlocked.Exchange(ref Items[id], null); + + if (deferred) + { + if (previous != null) + { + _dereferenceQueue.Enqueue(DereferenceRequest.Remove(texture)); + } + } + else + { + texture.DecrementReferenceCount(); + } + } + + /// <summary> + /// Queues a request to update a texture's mapping. + /// Mapping is updated later to avoid deleting the texture if it is still sparsely mapped. + /// </summary> + /// <param name="texture">Texture with potential mapping change</param> + /// <param name="id">ID in cache of texture with potential mapping change</param> + public void QueueUpdateMapping(Texture texture, int id) + { + if (Interlocked.Exchange(ref Items[id], null) == texture) + { + _dereferenceQueue.Enqueue(DereferenceRequest.Remap(texture, id)); + } + } + + /// <summary> + /// Process the dereference queue, decrementing the reference count for each texture in it. + /// This is used to ensure that texture disposal happens on the render thread. + /// </summary> + /// <param name="id">The ID of the entry that triggered this method</param> + /// <returns>Texture that matches the entry ID if it has been readded to the cache.</returns> + private Texture ProcessDereferenceQueue(int id = -1) + { + while (_dereferenceQueue.TryDequeue(out DereferenceRequest request)) + { + Texture texture = request.Texture; + + // Unmapped storage textures can swap their ranges. The texture must be storage with no views or dependencies. + // TODO: Would need to update ranges on views, or guarantee that ones where the range changes can be instantly deleted. + + if (request.IsRemapped && texture.Group.Storage == texture && !texture.HasViews && !texture.Group.HasCopyDependencies) + { + // Has the mapping for this texture changed? + ref readonly TextureDescriptor descriptor = ref GetDescriptorRef(request.ID); + + ulong address = descriptor.UnpackAddress(); + + MultiRange range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size); + + // If the texture is not mapped at all, delete its reference. + + if (range.Count == 1 && range.GetSubRange(0).Address == MemoryManager.PteUnmapped) + { + texture.DecrementReferenceCount(); + continue; + } + + Items[request.ID] = texture; + + // Create a new pool reference, as the last one was removed on unmap. + + texture.IncrementReferenceCount(this, request.ID, address); + texture.DecrementReferenceCount(); + + // Refetch the range. Changes since the last check could have been lost + // as the cache entry was not restored (required to queue mapping change). + + range = _channel.MemoryManager.GetPhysicalRegions(address, texture.Size); + + if (!range.Equals(texture.Range)) + { + // Part of the texture was mapped or unmapped. Replace the range and regenerate tracking handles. + if (!_channel.MemoryManager.Physical.TextureCache.UpdateMapping(texture, range)) + { + // Texture could not be remapped due to a collision, just delete it. + if (Interlocked.Exchange(ref Items[request.ID], null) != null) + { + // If this is null, a request was already queued to decrement reference. + texture.DecrementReferenceCount(this, request.ID); + } + continue; + } + } + + if (request.ID == id) + { + return texture; + } + } + else + { + texture.DecrementReferenceCount(); + } + } + + return null; + } + + /// <summary> + /// Implementation of the texture pool range invalidation. + /// </summary> + /// <param name="address">Start address of the range of the texture pool</param> + /// <param name="size">Size of the range being invalidated</param> + protected override void InvalidateRangeImpl(ulong address, ulong size) + { + ProcessDereferenceQueue(); + + ulong endAddress = address + size; + + for (; address < endAddress; address += DescriptorSize) + { + int id = (int)((address - Address) / DescriptorSize); + + Texture texture = Items[id]; + + if (texture != null) + { + ref TextureDescriptor cachedDescriptor = ref DescriptorCache[id]; + ref readonly TextureDescriptor descriptor = ref GetDescriptorRefAddress(address); + + // If the descriptors are the same, the texture is the same, + // we don't need to remove as it was not modified. Just continue. + if (descriptor.Equals(ref cachedDescriptor)) + { + continue; + } + + if (texture.HasOneReference()) + { + _channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor); + } + + if (Interlocked.Exchange(ref Items[id], null) != null) + { + texture.DecrementReferenceCount(this, id); + } + } + } + } + + /// <summary> + /// Gets texture information from a texture descriptor. + /// </summary> + /// <param name="descriptor">The texture descriptor</param> + /// <param name="layerSize">Layer size for textures using a sub-range of mipmap levels, otherwise 0</param> + /// <returns>The texture information</returns> + private TextureInfo GetInfo(in TextureDescriptor descriptor, out int layerSize) + { + int depthOrLayers = descriptor.UnpackDepth(); + int levels = descriptor.UnpackLevels(); + + TextureMsaaMode msaaMode = descriptor.UnpackTextureMsaaMode(); + + int samplesInX = msaaMode.SamplesInX(); + int samplesInY = msaaMode.SamplesInY(); + + int stride = descriptor.UnpackStride(); + + TextureDescriptorType descriptorType = descriptor.UnpackTextureDescriptorType(); + + bool isLinear = descriptorType == TextureDescriptorType.Linear; + + Target target = descriptor.UnpackTextureTarget().Convert((samplesInX | samplesInY) != 1); + + int width = target == Target.TextureBuffer ? descriptor.UnpackBufferTextureWidth() : descriptor.UnpackWidth(); + int height = descriptor.UnpackHeight(); + + if (target == Target.Texture2DMultisample || target == Target.Texture2DMultisampleArray) + { + // This is divided back before the backend texture is created. + width *= samplesInX; + height *= samplesInY; + } + + // We use 2D targets for 1D textures as that makes texture cache + // management easier. We don't know the target for render target + // and copies, so those would normally use 2D targets, which are + // not compatible with 1D targets. By doing that we also allow those + // to match when looking for compatible textures on the cache. + if (target == Target.Texture1D) + { + target = Target.Texture2D; + height = 1; + } + else if (target == Target.Texture1DArray) + { + target = Target.Texture2DArray; + height = 1; + } + + uint format = descriptor.UnpackFormat(); + bool srgb = descriptor.UnpackSrgb(); + + ulong gpuVa = descriptor.UnpackAddress(); + + if (!FormatTable.TryGetTextureFormat(format, srgb, out FormatInfo formatInfo)) + { + if (gpuVa != 0 && (int)format > 0) + { + Logger.Error?.Print(LogClass.Gpu, $"Invalid texture format 0x{format:X} (sRGB: {srgb})."); + } + + formatInfo = FormatInfo.Default; + } + + int gobBlocksInY = descriptor.UnpackGobBlocksInY(); + int gobBlocksInZ = descriptor.UnpackGobBlocksInZ(); + + int gobBlocksInTileX = descriptor.UnpackGobBlocksInTileX(); + + layerSize = 0; + + int minLod = descriptor.UnpackBaseLevel(); + int maxLod = descriptor.UnpackMaxLevelInclusive(); + + // Linear textures don't support mipmaps, so we don't handle this case here. + if ((minLod != 0 || maxLod + 1 != levels) && target != Target.TextureBuffer && !isLinear) + { + int depth = TextureInfo.GetDepth(target, depthOrLayers); + int layers = TextureInfo.GetLayers(target, depthOrLayers); + + SizeInfo sizeInfo = SizeCalculator.GetBlockLinearTextureSize( + width, + height, + depth, + levels, + layers, + formatInfo.BlockWidth, + formatInfo.BlockHeight, + formatInfo.BytesPerPixel, + gobBlocksInY, + gobBlocksInZ, + gobBlocksInTileX); + + layerSize = sizeInfo.LayerSize; + + if (minLod != 0 && minLod < levels) + { + // If the base level is not zero, we additionally add the mip level offset + // to the address, this allows the texture manager to find the base level from the + // address if there is a overlapping texture on the cache that can contain the new texture. + gpuVa += (ulong)sizeInfo.GetMipOffset(minLod); + + width = Math.Max(1, width >> minLod); + height = Math.Max(1, height >> minLod); + + if (target == Target.Texture3D) + { + depthOrLayers = Math.Max(1, depthOrLayers >> minLod); + } + + (gobBlocksInY, gobBlocksInZ) = SizeCalculator.GetMipGobBlockSizes(height, depth, formatInfo.BlockHeight, gobBlocksInY, gobBlocksInZ); + } + + levels = (maxLod - minLod) + 1; + } + + SwizzleComponent swizzleR = descriptor.UnpackSwizzleR().Convert(); + SwizzleComponent swizzleG = descriptor.UnpackSwizzleG().Convert(); + SwizzleComponent swizzleB = descriptor.UnpackSwizzleB().Convert(); + SwizzleComponent swizzleA = descriptor.UnpackSwizzleA().Convert(); + + DepthStencilMode depthStencilMode = GetDepthStencilMode( + formatInfo.Format, + swizzleR, + swizzleG, + swizzleB, + swizzleA); + + if (formatInfo.Format.IsDepthOrStencil()) + { + swizzleR = SwizzleComponent.Red; + swizzleG = SwizzleComponent.Red; + swizzleB = SwizzleComponent.Red; + + if (depthStencilMode == DepthStencilMode.Depth) + { + swizzleA = SwizzleComponent.One; + } + else + { + swizzleA = SwizzleComponent.Red; + } + } + + return new TextureInfo( + gpuVa, + width, + height, + depthOrLayers, + levels, + samplesInX, + samplesInY, + stride, + isLinear, + gobBlocksInY, + gobBlocksInZ, + gobBlocksInTileX, + target, + formatInfo, + depthStencilMode, + swizzleR, + swizzleG, + swizzleB, + swizzleA); + } + + /// <summary> + /// Gets the texture depth-stencil mode, based on the swizzle components of each color channel. + /// The depth-stencil mode is determined based on how the driver sets those parameters. + /// </summary> + /// <param name="format">The format of the texture</param> + /// <param name="components">The texture swizzle components</param> + /// <returns>The depth-stencil mode</returns> + private static DepthStencilMode GetDepthStencilMode(Format format, params SwizzleComponent[] components) + { + // R = Depth, G = Stencil. + // On 24-bits depth formats, this is inverted (Stencil is R etc). + // NVN setup: + // For depth, A is set to 1.0f, the other components are set to Depth. + // For stencil, all components are set to Stencil. + SwizzleComponent component = components[0]; + + for (int index = 1; index < 4 && !IsRG(component); index++) + { + component = components[index]; + } + + if (!IsRG(component)) + { + return DepthStencilMode.Depth; + } + + if (format == Format.D24UnormS8Uint) + { + return component == SwizzleComponent.Red + ? DepthStencilMode.Stencil + : DepthStencilMode.Depth; + } + else + { + return component == SwizzleComponent.Red + ? DepthStencilMode.Depth + : DepthStencilMode.Stencil; + } + } + + /// <summary> + /// Checks if the swizzle component is equal to the red or green channels. + /// </summary> + /// <param name="component">The swizzle component to check</param> + /// <returns>True if the swizzle component is equal to the red or green, false otherwise</returns> + private static bool IsRG(SwizzleComponent component) + { + return component == SwizzleComponent.Red || + component == SwizzleComponent.Green; + } + + /// <summary> + /// Decrements the reference count of the texture. + /// This indicates that the texture pool is not using it anymore. + /// </summary> + /// <param name="item">The texture to be deleted</param> + protected override void Delete(Texture item) + { + item?.DecrementReferenceCount(this); + } + + public override void Dispose() + { + ProcessDereferenceQueue(); + + base.Dispose(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TexturePoolCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TexturePoolCache.cs new file mode 100644 index 00000000..0017f4cc --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TexturePoolCache.cs @@ -0,0 +1,30 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture pool cache. + /// This can keep multiple texture pools, and return the current one as needed. + /// It is useful for applications that uses multiple texture pools. + /// </summary> + class TexturePoolCache : PoolCache<TexturePool> + { + /// <summary> + /// Constructs a new instance of the texture pool. + /// </summary> + /// <param name="context">GPU context that the texture pool belongs to</param> + public TexturePoolCache(GpuContext context) : base(context) + { + } + + /// <summary> + /// Creates a new instance of the texture pool. + /// </summary> + /// <param name="context">GPU context that the texture pool belongs to</param> + /// <param name="channel">GPU channel that the texture pool belongs to</param> + /// <param name="address">Address of the texture pool in guest memory</param> + /// <param name="maximumId">Maximum texture ID of the texture pool (equal to maximum textures minus one)</param> + protected override TexturePool CreatePool(GpuContext context, GpuChannel channel, ulong address, int maximumId) + { + return new TexturePool(context, channel, address, maximumId); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs new file mode 100644 index 00000000..b937f577 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// The scale mode for a given texture. + /// Blacklisted textures cannot be scaled, Eligible textures have not been scaled yet, + /// and Scaled textures have been scaled already. + /// Undesired textures will stay at 1x until a situation where they must match a scaled texture. + /// </summary> + enum TextureScaleMode + { + Eligible = 0, + Scaled = 1, + Blacklisted = 2, + Undesired = 3 + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs new file mode 100644 index 00000000..890bf173 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs @@ -0,0 +1,17 @@ +using System; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture search flags, defines texture information comparison rules. + /// </summary> + [Flags] + enum TextureSearchFlags + { + None = 0, + ForSampler = 1 << 1, + ForCopy = 1 << 2, + WithUpscale = 1 << 3, + NoCreate = 1 << 4 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureTarget.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureTarget.cs new file mode 100644 index 00000000..5e0a0721 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureTarget.cs @@ -0,0 +1,81 @@ +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// Texture target. + /// </summary> + enum TextureTarget : byte + { + Texture1D, + Texture2D, + Texture3D, + Cubemap, + Texture1DArray, + Texture2DArray, + TextureBuffer, + Texture2DRect, + CubemapArray + } + + static class TextureTargetConverter + { + /// <summary> + /// Converts the texture target enum to a host compatible, Graphics Abstraction Layer enum. + /// </summary> + /// <param name="target">The target enum to convert</param> + /// <param name="isMultisample">True if the texture is a multisampled texture</param> + /// <returns>The host compatible texture target</returns> + public static Target Convert(this TextureTarget target, bool isMultisample) + { + if (isMultisample) + { + switch (target) + { + case TextureTarget.Texture2D: return Target.Texture2DMultisample; + case TextureTarget.Texture2DArray: return Target.Texture2DMultisampleArray; + } + } + else + { + switch (target) + { + case TextureTarget.Texture1D: return Target.Texture1D; + case TextureTarget.Texture2D: return Target.Texture2D; + case TextureTarget.Texture2DRect: return Target.Texture2D; + case TextureTarget.Texture3D: return Target.Texture3D; + case TextureTarget.Texture1DArray: return Target.Texture1DArray; + case TextureTarget.Texture2DArray: return Target.Texture2DArray; + case TextureTarget.Cubemap: return Target.Cubemap; + case TextureTarget.CubemapArray: return Target.CubemapArray; + case TextureTarget.TextureBuffer: return Target.TextureBuffer; + } + } + + return Target.Texture1D; + } + + /// <summary> + /// Converts the texture target enum to a shader sampler type. + /// </summary> + /// <param name="target">The target enum to convert</param> + /// <returns>The shader sampler type</returns> + public static SamplerType ConvertSamplerType(this TextureTarget target) + { + return target switch + { + TextureTarget.Texture1D => SamplerType.Texture1D, + TextureTarget.Texture2D => SamplerType.Texture2D, + TextureTarget.Texture3D => SamplerType.Texture3D, + TextureTarget.Cubemap => SamplerType.TextureCube, + TextureTarget.Texture1DArray => SamplerType.Texture1D | SamplerType.Array, + TextureTarget.Texture2DArray => SamplerType.Texture2D | SamplerType.Array, + TextureTarget.TextureBuffer => SamplerType.TextureBuffer, + TextureTarget.Texture2DRect => SamplerType.Texture2D, + TextureTarget.CubemapArray => SamplerType.TextureCube | SamplerType.Array, + _ => SamplerType.Texture2D + }; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs new file mode 100644 index 00000000..b89936eb --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs @@ -0,0 +1,14 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// <summary> + /// The level of view compatibility one texture has to another. + /// Values are increasing in compatibility from 0 (incompatible). + /// </summary> + enum TextureViewCompatibility + { + Incompatible = 0, + LayoutIncompatible, + CopyOnly, + Full + } +} |
