From 5d69d9103ef423719619658dc3378869692a5064 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Thu, 10 Sep 2020 20:44:04 +0100 Subject: [PATCH] Texture/Buffer Memory Management Improvements (#1408) * Initial implementation. Still pending better valid-overlap handling, disposed pool, compressed format flush fix. * Very messy backend resource cache. * Oops * Dispose -> Release * Improve Release/Dispose. * More rule refinement. * View compatibility levels as an enum - you can always know if a view is only copy compatible. * General cleanup. Use locking on the resource cache, as it is likely to be used by other threads in future. * Rename resource cache to resource pool. * Address some of the smaller nits. * Fix regression with MK8 lens flare Texture flushes done the old way should trigger memory tracking. * Use TextureCreateInfo as a key. It now implements IEquatable and generates a hashcode based on width/height. * Fix size change for compressed+non-compressed view combos. Before, this could set either the compressed or non compressed texture with a size with the wrong size, depending on which texture had its size changed. This caused exceptions when flushing the texture. Now it correctly takes the block size into account, assuming that these textures are only related because a pixel in the non-compressed texture represents a block in the compressed one. * Implement JD's suggestion for HashCode Combine Co-authored-by: jduncanator <1518948+jduncanator@users.noreply.github.com> * Address feedback * Address feedback. Co-authored-by: jduncanator <1518948+jduncanator@users.noreply.github.com> --- Ryujinx.Cpu/MemoryManager.cs | 30 +- Ryujinx.Graphics.GAL/IRenderer.cs | 2 + Ryujinx.Graphics.GAL/ITexture.cs | 3 +- Ryujinx.Graphics.GAL/TextureCreateInfo.cs | 26 +- Ryujinx.Graphics.Gpu/Engine/Methods.cs | 1 + Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs | 31 +++ Ryujinx.Graphics.Gpu/Image/Texture.cs | 257 +++++++++++++----- .../Image/TextureCompatibility.cs | 75 +++-- Ryujinx.Graphics.Gpu/Image/TextureManager.cs | 200 ++++++++++---- .../Image/TextureViewCompatibility.cs | 13 + Ryujinx.Graphics.Gpu/Memory/Buffer.cs | 3 +- Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs | 10 + .../Image/TextureBuffer.cs | 5 + .../Image/TextureStorage.cs | 33 ++- Ryujinx.Graphics.OpenGL/Image/TextureView.cs | 34 ++- Ryujinx.Graphics.OpenGL/Renderer.cs | 18 +- Ryujinx.Graphics.OpenGL/ResourcePool.cs | 122 +++++++++ Ryujinx.HLE/Switch.cs | 2 + 18 files changed, 725 insertions(+), 140 deletions(-) create mode 100644 Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs create mode 100644 Ryujinx.Graphics.OpenGL/ResourcePool.cs diff --git a/Ryujinx.Cpu/MemoryManager.cs b/Ryujinx.Cpu/MemoryManager.cs index 75ecca1ca7..abbeee5f85 100644 --- a/Ryujinx.Cpu/MemoryManager.cs +++ b/Ryujinx.Cpu/MemoryManager.cs @@ -145,10 +145,36 @@ namespace Ryujinx.Cpu return; } + MarkRegionAsModified(va, (ulong)data.Length); + + WriteImpl(va, data); + } + + /// + /// Writes data to CPU mapped memory, without tracking. + /// + /// Virtual address to write the data into + /// Data to be written + public void WriteUntracked(ulong va, ReadOnlySpan data) + { + if (data.Length == 0) + { + return; + } + + WriteImpl(va, data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + /// + /// Writes data to CPU mapped memory. + /// + /// Virtual address to write the data into + /// Data to be written + private void WriteImpl(ulong va, ReadOnlySpan data) + { try { - MarkRegionAsModified(va, (ulong)data.Length); - if (IsContiguousAndMapped(va, data.Length)) { data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length)); diff --git a/Ryujinx.Graphics.GAL/IRenderer.cs b/Ryujinx.Graphics.GAL/IRenderer.cs index fec8d3bea5..73fafe4953 100644 --- a/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/Ryujinx.Graphics.GAL/IRenderer.cs @@ -29,6 +29,8 @@ namespace Ryujinx.Graphics.GAL void UpdateCounters(); + void PreFrame(); + ICounterEvent ReportCounter(CounterType type, EventHandler resultHandler); void ResetCounter(CounterType type); diff --git a/Ryujinx.Graphics.GAL/ITexture.cs b/Ryujinx.Graphics.GAL/ITexture.cs index 1c5b6ba5f2..543f9de085 100644 --- a/Ryujinx.Graphics.GAL/ITexture.cs +++ b/Ryujinx.Graphics.GAL/ITexture.cs @@ -2,7 +2,7 @@ using System; namespace Ryujinx.Graphics.GAL { - public interface ITexture : IDisposable + public interface ITexture { int Width { get; } int Height { get; } @@ -17,5 +17,6 @@ namespace Ryujinx.Graphics.GAL void SetData(ReadOnlySpan data); void SetStorage(BufferRange buffer); + void Release(); } } \ No newline at end of file diff --git a/Ryujinx.Graphics.GAL/TextureCreateInfo.cs b/Ryujinx.Graphics.GAL/TextureCreateInfo.cs index d74ac62dd2..eedf58a0c5 100644 --- a/Ryujinx.Graphics.GAL/TextureCreateInfo.cs +++ b/Ryujinx.Graphics.GAL/TextureCreateInfo.cs @@ -3,7 +3,7 @@ using System; namespace Ryujinx.Graphics.GAL { - public struct TextureCreateInfo + public struct TextureCreateInfo : IEquatable { public int Width { get; } public int Height { get; } @@ -116,5 +116,29 @@ namespace Ryujinx.Graphics.GAL { return Math.Max(1, size >> level); } + + public override int GetHashCode() + { + return HashCode.Combine(Width, Height); + } + + bool IEquatable.Equals(TextureCreateInfo other) + { + return Width == other.Width && + Height == other.Height && + Depth == other.Depth && + Levels == other.Levels && + Samples == other.Samples && + BlockWidth == other.BlockWidth && + BlockHeight == other.BlockHeight && + BytesPerPixel == other.BytesPerPixel && + Format == other.Format && + DepthStencilMode == other.DepthStencilMode && + Target == other.Target && + SwizzleR == other.SwizzleR && + SwizzleG == other.SwizzleG && + SwizzleB == other.SwizzleB && + SwizzleA == other.SwizzleA; + } } } diff --git a/Ryujinx.Graphics.Gpu/Engine/Methods.cs b/Ryujinx.Graphics.Gpu/Engine/Methods.cs index 9497b04561..79ed3c9076 100644 --- a/Ryujinx.Graphics.Gpu/Engine/Methods.cs +++ b/Ryujinx.Graphics.Gpu/Engine/Methods.cs @@ -58,6 +58,7 @@ namespace Ryujinx.Graphics.Gpu.Engine TextureManager = new TextureManager(context); context.MemoryManager.MemoryUnmapped += _counterCache.MemoryUnmappedHandler; + context.MemoryManager.MemoryUnmapped += TextureManager.MemoryUnmappedHandler; } /// diff --git a/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs b/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs index d66eab93f0..634f9448aa 100644 --- a/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs +++ b/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using System.Collections; using System.Collections.Generic; @@ -40,6 +41,16 @@ namespace Ryujinx.Graphics.Gpu.Image { Texture oldestTexture = _textures.First.Value; + oldestTexture.SynchronizeMemory(); + + if (oldestTexture.IsModified) + { + // 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.Flush(false); + } + _textures.RemoveFirst(); oldestTexture.DecrementReferenceCount(); @@ -74,6 +85,26 @@ namespace Ryujinx.Graphics.Gpu.Image } } + public bool Remove(Texture texture, bool flush) + { + if (texture.CacheNode == null) + { + return false; + } + + // Remove our reference to this texture. + if (flush && texture.IsModified) + { + texture.Flush(false); + } + + _textures.Remove(texture.CacheNode); + + texture.CacheNode = null; + + return texture.DecrementReferenceCount(); + } + public IEnumerator GetEnumerator() { return _textures.GetEnumerator(); diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs index 50b184d518..fe129f526c 100644 --- a/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -39,6 +39,13 @@ namespace Ryujinx.Graphics.Gpu.Image /// public TextureScaleMode ScaleMode { get; private set; } + /// + /// Set when a texture has been modified since it was last flushed. + /// + public bool IsModified { get; internal set; } + + private bool _everModified; + private int _depth; private int _layers; private int _firstLayer; @@ -121,7 +128,7 @@ namespace Ryujinx.Graphics.Gpu.Image ScaleFactor = scaleFactor; ScaleMode = scaleMode; - _hasData = true; + InitializeData(true); } /// @@ -137,16 +144,6 @@ namespace Ryujinx.Graphics.Gpu.Image ScaleMode = scaleMode; InitializeTexture(context, info, sizeInfo); - - TextureCreateInfo createInfo = TextureManager.GetCreateInfo(info, context.Capabilities); - - HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor); - - if (scaleMode == TextureScaleMode.Scaled) - { - SynchronizeMemory(); // Load the data and then scale it up. - SetScale(GraphicsConfig.ResScale); - } } /// @@ -171,6 +168,47 @@ namespace Ryujinx.Graphics.Gpu.Image _views = new List(); } + /// + /// 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. + /// + /// True if the texture is a view, false otherwise + /// True if the texture is to be initialized with data + public void InitializeData(bool isView, bool withData = false) + { + if (withData) + { + Debug.Assert(!isView); + + TextureCreateInfo createInfo = TextureManager.GetCreateInfo(Info, _context.Capabilities); + HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor); + + SynchronizeMemory(); // Load the data. + if (ScaleMode == TextureScaleMode.Scaled) + { + SetScale(GraphicsConfig.ResScale); // Scale the data up. + } + } + else + { + // Don't update this texture the next time we synchronize. + ConsumeModified(); + _hasData = true; + + if (!isView) + { + 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 = TextureManager.GetCreateInfo(Info, _context.Capabilities); + HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor); + } + } + } + /// /// Create a texture view from this texture. /// A texture view is defined as a child texture, from a sub-range of their parent texture. @@ -238,6 +276,9 @@ namespace Ryujinx.Graphics.Gpu.Image /// The new texture depth (for 3D textures) or layers (for layered textures) public void ChangeSize(int width, int height, int depthOrLayers) { + int blockWidth = Info.FormatInfo.BlockWidth; + int blockHeight = Info.FormatInfo.BlockHeight; + width <<= _firstLevel; height <<= _firstLevel; @@ -250,7 +291,7 @@ namespace Ryujinx.Graphics.Gpu.Image depthOrLayers = _viewStorage.Info.DepthOrLayers; } - _viewStorage.RecreateStorageOrView(width, height, depthOrLayers); + _viewStorage.RecreateStorageOrView(width, height, blockWidth, blockHeight, depthOrLayers); foreach (Texture view in _viewStorage._views) { @@ -268,10 +309,28 @@ namespace Ryujinx.Graphics.Gpu.Image viewDepthOrLayers = view.Info.DepthOrLayers; } - view.RecreateStorageOrView(viewWidth, viewHeight, viewDepthOrLayers); + view.RecreateStorageOrView(viewWidth, viewHeight, blockWidth, blockHeight, viewDepthOrLayers); } } + /// + /// Recreates the texture storage (or view, in the case of child textures) of this texture. + /// This allows recreating the texture with a new size. + /// A copy is automatically performed from the old to the new texture. + /// + /// The new texture width + /// The new texture height + /// The block width related to the given width + /// The block height related to the given height + /// The new texture depth (for 3D textures) or layers (for layered textures) + private void RecreateStorageOrView(int width, int height, int blockWidth, int blockHeight, int depthOrLayers) + { + RecreateStorageOrView( + BitUtils.DivRoundUp(width * Info.FormatInfo.BlockWidth, blockWidth), + BitUtils.DivRoundUp(height * Info.FormatInfo.BlockHeight, blockHeight), + depthOrLayers); + } + /// /// Recreates the texture storage (or view, in the case of child textures) of this texture. /// This allows recreating the texture with a new size. @@ -388,8 +447,8 @@ namespace Ryujinx.Graphics.Gpu.Image from.CopyTo(to, new Extents2D(0, 0, from.Width, from.Height), new Extents2D(0, 0, to.Width, to.Height), true); - from.Dispose(); - to.Dispose(); + from.Release(); + to.Release(); } } @@ -462,6 +521,16 @@ namespace Ryujinx.Graphics.Gpu.Image } } + /// + /// Checks if the memory for this texture was modified, and returns true if it was. + /// The modified flags are consumed as a result. + /// + /// True if the texture was modified, false otherwise. + public bool ConsumeModified() + { + return _context.PhysicalMemory.QueryModified(Address, Size, ResourceName.Texture, _modifiedRanges) > 0; + } + /// /// Synchronizes guest and host memory. /// This will overwrite the texture data with the texture data on the guest memory, if a CPU @@ -494,15 +563,15 @@ namespace Ryujinx.Graphics.Gpu.Image ReadOnlySpan data = _context.PhysicalMemory.GetSpan(Address, (int)Size); - // If the texture was modified by the host GPU, we do partial invalidation + // If the texture was ever modified by the host GPU, we do partial invalidation // of the texture by getting GPU data and merging in the pages of memory // that were modified. // Note that if ASTC is not supported by the GPU we can't read it back since // it will use a different format. Since applications shouldn't be writing // ASTC textures from the GPU anyway, ignoring it should be safe. - if (_context.Methods.TextureManager.IsTextureModified(this) && !Info.FormatInfo.Format.IsAstc()) + if (_everModified && !Info.FormatInfo.Format.IsAstc()) { - Span gpuData = GetTextureDataFromGpu(); + Span gpuData = GetTextureDataFromGpu(true); ulong endAddress = Address + Size; @@ -533,6 +602,8 @@ namespace Ryujinx.Graphics.Gpu.Image data = gpuData; } + IsModified = false; + data = ConvertToHostCompatibleFormat(data); HostTexture.SetData(data); @@ -607,10 +678,24 @@ namespace Ryujinx.Graphics.Gpu.Image /// 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. /// - public void Flush() + /// Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either. + public void Flush(bool tracked = true) { - BlacklistScale(); - _context.PhysicalMemory.Write(Address, GetTextureDataFromGpu()); + IsModified = false; + + if (Info.FormatInfo.Format.IsAstc()) + { + return; // Flushing this format is not supported, as it may have been converted to another host format. + } + + if (tracked) + { + _context.PhysicalMemory.Write(Address, GetTextureDataFromGpu(tracked)); + } + else + { + _context.PhysicalMemory.WriteUntracked(Address, GetTextureDataFromGpu(tracked)); + } } /// @@ -621,10 +706,26 @@ namespace Ryujinx.Graphics.Gpu.Image /// This is not cheap, avoid doing that unless strictly needed. /// /// Host texture data - private Span GetTextureDataFromGpu() + private Span GetTextureDataFromGpu(bool blacklist) { - BlacklistScale(); - Span data = HostTexture.GetData(); + Span data; + + if (blacklist) + { + BlacklistScale(); + data = HostTexture.GetData(); + } + else if (ScaleFactor != 1f) + { + float scale = ScaleFactor; + SetScale(1f); + data = HostTexture.GetData(); + SetScale(scale); + } + else + { + data = HostTexture.GetData(); + } if (Info.IsLinear) { @@ -713,31 +814,12 @@ namespace Ryujinx.Graphics.Gpu.Image /// Texture view size /// Texture view initial layer on this texture /// Texture view first mipmap level on this texture - /// True if a view with the given parameters can be created from this texture, false otherwise - public bool IsViewCompatible( + /// The level of compatiblilty a view with the given parameters created from this texture has + public TextureViewCompatibility IsViewCompatible( TextureInfo info, ulong size, out int firstLayer, out int firstLevel) - { - return IsViewCompatible(info, size, isCopy: false, out firstLayer, out firstLevel); - } - - /// - /// Check if it's possible to create a view, with the given parameters, from this texture. - /// - /// Texture view information - /// Texture view size - /// True to check for copy compability, instead of view compatibility - /// Texture view initial layer on this texture - /// Texture view first mipmap level on this texture - /// True if a view with the given parameters can be created from this texture, false otherwise - public bool IsViewCompatible( - TextureInfo info, - ulong size, - bool isCopy, - out int firstLayer, - out int firstLevel) { // Out of range. if (info.Address < Address || info.Address + size > EndAddress) @@ -745,38 +827,46 @@ namespace Ryujinx.Graphics.Gpu.Image firstLayer = 0; firstLevel = 0; - return false; + return TextureViewCompatibility.Incompatible; } int offset = (int)(info.Address - Address); if (!_sizeInfo.FindView(offset, (int)size, out firstLayer, out firstLevel)) { - return false; + return TextureViewCompatibility.Incompatible; } if (!TextureCompatibility.ViewLayoutCompatible(Info, info, firstLevel)) { - return false; + return TextureViewCompatibility.Incompatible; } if (!TextureCompatibility.ViewFormatCompatible(Info, info)) { - return false; + return TextureViewCompatibility.Incompatible; } - if (!TextureCompatibility.ViewSizeMatches(Info, info, firstLevel, isCopy)) - { - return false; - } + TextureViewCompatibility result = TextureViewCompatibility.Full; - if (!TextureCompatibility.ViewTargetCompatible(Info, info, isCopy)) - { - return false; - } + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSizeMatches(Info, info, firstLevel)); + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info)); - return Info.SamplesInX == info.SamplesInX && - Info.SamplesInY == info.SamplesInY; + return (Info.SamplesInX == info.SamplesInX && + Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible; + } + + /// + /// Checks if the view format is compatible with this texture format. + /// 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. + /// + /// Texture information of the texture view + /// True if the formats are compatible, false otherwise + private bool ViewFormatCompatible(TextureInfo info) + { + return TextureCompatibility.FormatCompatible(Info.FormatInfo, info.FormatInfo); } /// @@ -902,7 +992,15 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { + IsModified = true; + _everModified = true; + Modified?.Invoke(this); + + if (_viewStorage != this) + { + _viewStorage.SignalModified(); + } } /// @@ -927,6 +1025,29 @@ namespace Ryujinx.Graphics.Gpu.Image return Address < address + size && address < EndAddress; } + /// + /// Determine if any of our child textures are compaible as views of the given texture. + /// + /// The texture to check against + /// True if any child is view compatible, false otherwise + public bool HasViewCompatibleChild(Texture texture) + { + if (_viewStorage != this || _views.Count == 0) + { + return false; + } + + foreach (Texture view in _views) + { + if (texture.IsViewCompatible(view.Info, view.Size, out int _, out int _) != TextureViewCompatibility.Incompatible) + { + return true; + } + } + + return false; + } + /// /// Increments the texture reference count. /// @@ -939,7 +1060,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// Decrements the texture reference count. /// When the reference count hits zero, the texture may be deleted and can't be used anymore. /// - public void DecrementReferenceCount() + /// True if the texture is now referenceless, false otherwise + public bool DecrementReferenceCount() { int newRefCount = --_referenceCount; @@ -956,6 +1078,8 @@ namespace Ryujinx.Graphics.Gpu.Image Debug.Assert(newRefCount >= 0); DeleteIfNotUsed(); + + return newRefCount <= 0; } /// @@ -980,12 +1104,21 @@ namespace Ryujinx.Graphics.Gpu.Image /// private void DisposeTextures() { - HostTexture.Dispose(); + HostTexture.Release(); - _arrayViewTexture?.Dispose(); + _arrayViewTexture?.Release(); _arrayViewTexture = null; } + /// + /// Called when the memory for this texture has been unmapped. + /// Calls are from non-gpu threads. + /// + public void Unmapped() + { + IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped. + } + /// /// Performs texture disposal, deleting the texture. /// diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index cc7b0dc275..e8e3c2c24d 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -147,28 +147,51 @@ namespace Ryujinx.Graphics.Gpu.Image } /// - /// Checks if the view sizes of a two given texture informations match. + /// Obtain the minimum compatibility level of two provided view compatibility results. + /// + /// The first compatibility level + /// The second compatibility level + /// The minimum compatibility level of two provided view compatibility results + public static TextureViewCompatibility PropagateViewCompatibility(TextureViewCompatibility first, TextureViewCompatibility second) + { + if (first == TextureViewCompatibility.Incompatible || second == TextureViewCompatibility.Incompatible) + { + return TextureViewCompatibility.Incompatible; + } + else if (first == TextureViewCompatibility.CopyOnly || second == TextureViewCompatibility.CopyOnly) + { + return TextureViewCompatibility.CopyOnly; + } + else + { + return TextureViewCompatibility.Full; + } + } + + /// + /// Checks if the sizes of two given textures are view compatible. /// /// Texture information of the texture view /// Texture information of the texture view to match against /// Mipmap level of the texture view in relation to this texture - /// True to check for copy compatibility rather than view compatibility /// True if the sizes are compatible, false otherwise - public static bool ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level, bool isCopy) + public static TextureViewCompatibility ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level) { Size size = GetAlignedSize(lhs, level); Size otherSize = GetAlignedSize(rhs); + 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 (!isCopy && rhs.Target == Target.Texture3D && size.Depth != otherSize.Depth) + if (rhs.Target == Target.Texture3D && size.Depth != otherSize.Depth) { - return false; + result = TextureViewCompatibility.CopyOnly; } - return size.Width == otherSize.Width && - size.Height == otherSize.Height; + return (size.Width == otherSize.Width && + size.Height == otherSize.Height) ? result : TextureViewCompatibility.Incompatible; } /// @@ -330,38 +353,48 @@ namespace Ryujinx.Graphics.Gpu.Image /// Texture information of the texture view /// True to check for copy rather than view compatibility /// True if the targets are compatible, false otherwise - public static bool ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs, bool isCopy) + public static TextureViewCompatibility ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs) { + bool result = false; switch (lhs.Target) { case Target.Texture1D: case Target.Texture1DArray: - return rhs.Target == Target.Texture1D || - rhs.Target == Target.Texture1DArray; + result = rhs.Target == Target.Texture1D || + rhs.Target == Target.Texture1DArray; + break; case Target.Texture2D: - return rhs.Target == Target.Texture2D || - rhs.Target == Target.Texture2DArray; + result = rhs.Target == Target.Texture2D || + rhs.Target == Target.Texture2DArray; + break; case Target.Texture2DArray: case Target.Cubemap: case Target.CubemapArray: - return rhs.Target == Target.Texture2D || - rhs.Target == Target.Texture2DArray || - rhs.Target == Target.Cubemap || - rhs.Target == Target.CubemapArray; + result = rhs.Target == Target.Texture2D || + rhs.Target == Target.Texture2DArray || + rhs.Target == Target.Cubemap || + rhs.Target == Target.CubemapArray; + break; case Target.Texture2DMultisample: case Target.Texture2DMultisampleArray: - return rhs.Target == Target.Texture2DMultisample || - rhs.Target == Target.Texture2DMultisampleArray; + result = rhs.Target == Target.Texture2DMultisample || + rhs.Target == Target.Texture2DMultisampleArray; + break; case Target.Texture3D: - return rhs.Target == Target.Texture3D || - (rhs.Target == Target.Texture2D && isCopy); + if (rhs.Target == Target.Texture2D) + { + return TextureViewCompatibility.CopyOnly; + } + + result = rhs.Target == Target.Texture3D; + break; } - return false; + return result ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible; } /// diff --git a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index cab76da1bc..0b1d38d109 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -14,6 +14,20 @@ namespace Ryujinx.Graphics.Gpu.Image /// class TextureManager : IDisposable { + private 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; @@ -33,6 +47,7 @@ namespace Ryujinx.Graphics.Gpu.Image private readonly RangeList _textures; private Texture[] _textureOverlaps; + private OverlapInfo[] _overlapInfo; private readonly AutoDeleteCache _cache; @@ -64,6 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Image _textures = new RangeList(); _textureOverlaps = new Texture[OverlapsBufferInitialCapacity]; + _overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity]; _cache = new AutoDeleteCache(); @@ -407,6 +423,27 @@ namespace Ryujinx.Graphics.Gpu.Image return true; } + /// + /// Handles removal of textures written to a memory region being unmapped. + /// + /// Sender object + /// Event arguments + public void MemoryUnmappedHandler(object sender, UnmapEventArgs e) + { + Texture[] overlaps = new Texture[10]; + int overlapCount; + + lock (_textures) + { + overlapCount = _textures.FindOverlaps(_context.MemoryManager.Translate(e.Address), e.Size, ref overlaps); + } + + for (int i = 0; i < overlapCount; i++) + { + overlaps[i].Unmapped(); + } + } + /// /// Tries to find an existing texture, or create a new one if not found. /// @@ -618,8 +655,13 @@ namespace Ryujinx.Graphics.Gpu.Image scaleMode = (flags & TextureSearchFlags.WithUpscale) != 0 ? TextureScaleMode.Scaled : TextureScaleMode.Eligible; } - // Try to find a perfect texture match, with the same address and parameters. - int sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps); + int sameAddressOverlapsCount; + + lock (_textures) + { + // Try to find a perfect texture match, with the same address and parameters. + sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps); + } for (int index = 0; index < sameAddressOverlapsCount; index++) { @@ -681,8 +723,12 @@ namespace Ryujinx.Graphics.Gpu.Image // Find view compatible matches. ulong size = (ulong)sizeInfo.TotalSize; + int overlapsCount; - int overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps); + lock (_textures) + { + overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps); + } Texture texture = null; @@ -690,7 +736,7 @@ namespace Ryujinx.Graphics.Gpu.Image { Texture overlap = _textureOverlaps[index]; - if (overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel)) + if (overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel) == TextureViewCompatibility.Full) { if (!isSamplerTexture) { @@ -701,7 +747,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (IsTextureModified(overlap)) { - CacheTextureModified(texture); + texture.SignalModified(); } // The size only matters (and is only really reliable) when the @@ -721,65 +767,114 @@ namespace Ryujinx.Graphics.Gpu.Image { texture = new Texture(_context, info, sizeInfo, scaleMode); - // 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.SynchronizeMemory(); + // 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; + bool setData = isSamplerTexture || overlapsCount == 0; for (int index = 0; index < overlapsCount; index++) { Texture overlap = _textureOverlaps[index]; + bool overlapInCache = overlap.CacheNode != null; - if (texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel)) + TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel); + + if (compatibility != TextureViewCompatibility.Incompatible) { - TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, firstLevel); - - TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities); - - if (texture.ScaleFactor != overlap.ScaleFactor) + if (_overlapInfo.Length != _textureOverlaps.Length) { - // 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); + Array.Resize(ref _overlapInfo, _textureOverlaps.Length); } - ITexture newView = texture.HostTexture.CreateView(createInfo, firstLayer, firstLevel); - - overlap.HostTexture.CopyTo(newView, 0, 0); - - // Inherit modification from overlapping texture, do that before replacing - // the view since the replacement operation removes it from the list. - if (IsTextureModified(overlap)) - { - CacheTextureModified(texture); - } - - overlap.ReplaceView(texture, overlapInfo, newView, firstLayer, firstLevel); + _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); + _textureOverlaps[viewCompatible++] = overlap; } + else if (overlapInCache || !setData) + { + if (info.GobBlocksInZ > 1 && info.GobBlocksInZ == overlap.Info.GobBlocksInZ) + { + // Allow overlapping slices of 3D textures. Could be improved in future by making sure the textures don't overlap. + continue; + } + + // The overlap texture is going to contain garbage data after we draw, or is generally incompatible. + // If the texture cannot be entirely contained in the new address space, and one of its view children is compatible with us, + // it must be flushed before removal, so that the data is not lost. + + // 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 modified = overlap.ConsumeModified(); + + bool flush = overlapInCache && !modified && (overlap.Address < texture.Address || overlap.EndAddress > texture.EndAddress) && overlap.HasViewCompatibleChild(texture); + + setData |= modified || flush; + + if (overlapInCache) + { + _cache.Remove(overlap, flush); + } + } + } + + // 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); + + for (int index = 0; index < viewCompatible; index++) + { + Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; + + if (oInfo.Compatibility != TextureViewCompatibility.Full) + { + continue; // Copy only compatibilty. + } + + TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, oInfo.FirstLevel); + + TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities); + + 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); + } + + ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel); + + overlap.HostTexture.CopyTo(newView, 0, 0); + + // Inherit modification from overlapping texture, do that before replacing + // the view since the replacement operation removes it from the list. + if (IsTextureModified(overlap)) + { + texture.SignalModified(); + } + + overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel); } // If the texture is a 3D texture, we need to additionally copy any slice // of the 3D texture to the newly created 3D texture. if (info.Target == Target.Texture3D) { - for (int index = 0; index < overlapsCount; index++) + for (int index = 0; index < viewCompatible; index++) { Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; - if (texture.IsViewCompatible( - overlap.Info, - overlap.Size, - isCopy: true, - out int firstLayer, - out int firstLevel)) + if (oInfo.Compatibility != TextureViewCompatibility.Incompatible) { overlap.BlacklistScale(); - overlap.HostTexture.CopyTo(texture.HostTexture, firstLayer, firstLevel); + overlap.HostTexture.CopyTo(texture.HostTexture, oInfo.FirstLayer, oInfo.FirstLevel); if (IsTextureModified(overlap)) { - CacheTextureModified(texture); + texture.SignalModified(); } } } @@ -795,7 +890,10 @@ namespace Ryujinx.Graphics.Gpu.Image texture.Disposed += CacheTextureDisposed; } - _textures.Add(texture); + lock (_textures) + { + _textures.Add(texture); + } ShrinkOverlapsBufferIfNeeded(); @@ -818,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// The texture that was modified. private void CacheTextureModified(Texture texture) { + texture.IsModified = true; _modified.Add(texture); if (texture.Info.IsLinear) @@ -992,7 +1091,10 @@ namespace Ryujinx.Graphics.Gpu.Image { foreach (Texture texture in _modifiedLinear) { - texture.Flush(); + if (texture.IsModified) + { + texture.Flush(); + } } _modifiedLinear.Clear(); @@ -1007,7 +1109,7 @@ namespace Ryujinx.Graphics.Gpu.Image { foreach (Texture texture in _modified) { - if (texture.OverlapsWith(address, size)) + if (texture.OverlapsWith(address, size) && texture.IsModified) { texture.Flush(); } @@ -1024,7 +1126,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// The texture to be removed public void RemoveTextureFromCache(Texture texture) { - _textures.Remove(texture); + lock (_textures) + { + _textures.Remove(texture); + } } /// @@ -1033,10 +1138,13 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void Dispose() { - foreach (Texture texture in _textures) + lock (_textures) { - _modified.Remove(texture); - texture.Dispose(); + foreach (Texture texture in _textures) + { + _modified.Remove(texture); + texture.Dispose(); + } } } } diff --git a/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs new file mode 100644 index 0000000000..4671af4677 --- /dev/null +++ b/Ryujinx.Graphics.Gpu/Image/TextureViewCompatibility.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// The level of view compatibility one texture has to another. + /// Values are increasing in compatibility from 0 (incompatible). + /// + enum TextureViewCompatibility + { + Incompatible = 0, + CopyOnly, + Full + } +} diff --git a/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/Ryujinx.Graphics.Gpu/Memory/Buffer.cs index 5fe85d2ea3..2394f90d5f 100644 --- a/Ryujinx.Graphics.Gpu/Memory/Buffer.cs +++ b/Ryujinx.Graphics.Gpu/Memory/Buffer.cs @@ -151,7 +151,8 @@ namespace Ryujinx.Graphics.Gpu.Memory byte[] data = _context.Renderer.GetBufferData(Handle, offset, (int)size); - _context.PhysicalMemory.Write(address, data); + // TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers. + _context.PhysicalMemory.WriteUntracked(address, data); } /// diff --git a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs index 88beab8f18..ed3253698e 100644 --- a/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs +++ b/Ryujinx.Graphics.Gpu/Memory/PhysicalMemory.cs @@ -67,6 +67,16 @@ namespace Ryujinx.Graphics.Gpu.Memory _cpuMemory.Write(address, data); } + /// + /// Writes data to the application process, without any tracking. + /// + /// Address to write into + /// Data to be written + public void WriteUntracked(ulong address, ReadOnlySpan data) + { + _cpuMemory.WriteUntracked(address, data); + } + /// /// Checks if a specified virtual memory region has been modified by the CPU since the last call. /// diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs index 2c69571c3a..6df2b630c3 100644 --- a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs +++ b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs @@ -67,5 +67,10 @@ namespace Ryujinx.Graphics.OpenGL.Image Handle = 0; } } + + public void Release() + { + Dispose(); + } } } diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs b/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs index ed258aee16..635b6c2ce8 100644 --- a/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs +++ b/Ryujinx.Graphics.OpenGL/Image/TextureStorage.cs @@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.OpenGL.Image private int _viewsCount; + internal ITexture DefaultView { get; private set; } + public TextureStorage(Renderer renderer, TextureCreateInfo info, float scaleFactor) { _renderer = renderer; @@ -147,7 +149,9 @@ namespace Ryujinx.Graphics.OpenGL.Image public ITexture CreateDefaultView() { - return CreateView(Info, 0, 0); + DefaultView = CreateView(Info, 0, 0); + + return DefaultView; } public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) @@ -167,12 +171,37 @@ namespace Ryujinx.Graphics.OpenGL.Image // If we don't have any views, then the storage is now useless, delete it. if (--_viewsCount == 0) { - Dispose(); + if (DefaultView == null) + { + Dispose(); + } + else + { + // If the default view still exists, we can put it into the resource pool. + Release(); + } } } + /// + /// Release the TextureStorage to the resource pool without disposing its handle. + /// + public void Release() + { + _viewsCount = 1; // When we are used again, we will have the default view. + + _renderer.ResourcePool.AddTexture((TextureView)DefaultView); + } + + public void DeleteDefault() + { + DefaultView = null; + } + public void Dispose() { + DefaultView = null; + if (Handle != 0) { GL.DeleteTexture(Handle); diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 2d50eba5e1..04cadae73a 100644 --- a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -434,7 +434,7 @@ namespace Ryujinx.Graphics.OpenGL.Image throw new NotSupportedException(); } - public void Dispose() + private void DisposeHandles() { if (_incompatibleFormatView != null) { @@ -447,10 +447,38 @@ namespace Ryujinx.Graphics.OpenGL.Image { GL.DeleteTexture(Handle); - _parent.DecrementViewsCount(); - Handle = 0; } } + + /// + /// Release the view without necessarily disposing the parent if we are the default view. + /// This allows it to be added to the resource pool and reused later. + /// + public void Release() + { + bool hadHandle = Handle != 0; + + if (_parent.DefaultView != this) + { + DisposeHandles(); + } + + if (hadHandle) + { + _parent.DecrementViewsCount(); + } + } + + public void Dispose() + { + if (_parent.DefaultView == this) + { + // Remove the default view (us), so that the texture cannot be released to the cache. + _parent.DeleteDefault(); + } + + Release(); + } } } diff --git a/Ryujinx.Graphics.OpenGL/Renderer.cs b/Ryujinx.Graphics.OpenGL/Renderer.cs index ee2fe93d38..061821eb65 100644 --- a/Ryujinx.Graphics.OpenGL/Renderer.cs +++ b/Ryujinx.Graphics.OpenGL/Renderer.cs @@ -23,6 +23,8 @@ namespace Ryujinx.Graphics.OpenGL internal TextureCopy TextureCopy { get; } + internal ResourcePool ResourcePool { get; } + public string GpuVendor { get; private set; } public string GpuRenderer { get; private set; } public string GpuVersion { get; private set; } @@ -33,6 +35,7 @@ namespace Ryujinx.Graphics.OpenGL _counters = new Counters(); _window = new Window(this); TextureCopy = new TextureCopy(this); + ResourcePool = new ResourcePool(); } public IShader CompileShader(ShaderProgram shader) @@ -57,7 +60,14 @@ namespace Ryujinx.Graphics.OpenGL public ITexture CreateTexture(TextureCreateInfo info, float scaleFactor) { - return info.Target == Target.TextureBuffer ? new TextureBuffer(info) : new TextureStorage(this, info, scaleFactor).CreateDefaultView(); + if (info.Target == Target.TextureBuffer) + { + return new TextureBuffer(info); + } + else + { + return ResourcePool.GetTextureOrNull(info, scaleFactor) ?? new TextureStorage(this, info, scaleFactor).CreateDefaultView(); + } } public void DeleteBuffer(BufferHandle buffer) @@ -92,6 +102,11 @@ namespace Ryujinx.Graphics.OpenGL _counters.Update(); } + public void PreFrame() + { + ResourcePool.Tick(); + } + public ICounterEvent ReportCounter(CounterType type, EventHandler resultHandler) { return _counters.QueueReport(type, resultHandler); @@ -123,6 +138,7 @@ namespace Ryujinx.Graphics.OpenGL public void Dispose() { TextureCopy.Dispose(); + ResourcePool.Dispose(); _pipeline.Dispose(); _window.Dispose(); _counters.Dispose(); diff --git a/Ryujinx.Graphics.OpenGL/ResourcePool.cs b/Ryujinx.Graphics.OpenGL/ResourcePool.cs new file mode 100644 index 0000000000..57231cd659 --- /dev/null +++ b/Ryujinx.Graphics.OpenGL/ResourcePool.cs @@ -0,0 +1,122 @@ +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.OpenGL.Image; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.OpenGL +{ + class DisposedTexture + { + public TextureCreateInfo Info; + public TextureView View; + public float ScaleFactor; + public int RemainingFrames; + } + + /// + /// A structure for pooling resources that can be reused without recreation, such as textures. + /// + class ResourcePool : IDisposable + { + private const int DisposedLiveFrames = 2; + + private readonly object _lock = new object(); + private readonly Dictionary> _textures = new Dictionary>(); + + /// + /// Add a texture that is not being used anymore to the resource pool to be used later. + /// Both the texture's view and storage should be completely unused. + /// + /// The texture's view + public void AddTexture(TextureView view) + { + lock (_lock) + { + List list; + if (!_textures.TryGetValue(view.Info, out list)) + { + list = new List(); + _textures.Add(view.Info, list); + } + + list.Add(new DisposedTexture() + { + Info = view.Info, + View = view, + ScaleFactor = view.ScaleFactor, + RemainingFrames = DisposedLiveFrames + }); + } + } + + /// + /// Attempt to obtain a texture from the resource cache with the desired parameters. + /// + /// The creation info for the desired texture + /// The scale factor for the desired texture + /// A TextureView with the description specified, or null if one was not found. + public TextureView GetTextureOrNull(TextureCreateInfo info, float scaleFactor) + { + lock (_lock) + { + List list; + if (!_textures.TryGetValue(info, out list)) + { + return null; + } + + foreach (DisposedTexture texture in list) + { + if (scaleFactor == texture.ScaleFactor) + { + list.Remove(texture); + return texture.View; + } + } + + return null; + } + } + + /// + /// Update the pool, removing any resources that have expired. + /// + public void Tick() + { + lock (_lock) + { + foreach (List list in _textures.Values) + { + for (int i = 0; i < list.Count; i++) + { + DisposedTexture tex = list[i]; + + if (--tex.RemainingFrames < 0) + { + tex.View.Dispose(); + list.RemoveAt(i--); + } + } + } + } + } + + /// + /// Disposes the resource pool. + /// + public void Dispose() + { + lock (_lock) + { + foreach (List list in _textures.Values) + { + foreach (DisposedTexture texture in list) + { + texture.View.Dispose(); + } + } + _textures.Clear(); + } + } + } +} diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index df02e5e573..5401f1ccda 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -159,6 +159,8 @@ namespace Ryujinx.HLE public void ProcessFrame() { + Gpu.Renderer.PreFrame(); + Gpu.GPFifo.DispatchCalls(); }