aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Graphics.Gpu/Image
diff options
context:
space:
mode:
authorTSR Berry <20988865+TSRBerry@users.noreply.github.com>2023-04-08 01:22:00 +0200
committerMary <thog@protonmail.com>2023-04-27 23:51:14 +0200
commitcee712105850ac3385cd0091a923438167433f9f (patch)
tree4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Graphics.Gpu/Image
parentcd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff)
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Graphics.Gpu/Image')
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs256
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/FormatInfo.cs72
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/FormatTable.cs578
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/ITextureDescriptor.cs10
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/Pool.cs222
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/PoolCache.cs129
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/ReductionFilter.cs15
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/Sampler.cs115
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/SamplerDescriptor.cs260
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/SamplerMinFilter.cs11
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/SamplerMipFilter.cs12
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/SamplerPool.cs162
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/SamplerPoolCache.cs30
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/Texture.cs1705
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs73
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureBindingsManager.cs882
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs1180
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs911
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureComponent.cs43
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs37
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureDescriptor.cs273
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureDescriptorType.cs16
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs1611
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs554
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs381
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureManager.cs498
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureMatchQuality.cs9
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureMsaaMode.cs68
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TexturePool.cs603
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TexturePoolCache.cs30
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureScaleMode.cs16
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureSearchFlags.cs17
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureTarget.cs81
-rw-r--r--src/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs14
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
+ }
+}