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(); }