From cda659955ced1b16839cdd1e7fea1ef6f8d99041 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Sun, 9 Jan 2022 16:28:48 +0000 Subject: Texture Sync, incompatible overlap handling, data flush improvements. (#2971) * Initial test for texture sync * WIP new texture flushing setup * Improve rules for incompatible overlaps Fixes a lot of issues with Unreal Engine games. Still a few minor issues (some caused by dma fast path?) Needs docs and cleanup. * Cleanup, improvements Improve rules for fast DMA * Small tweak to group together flushes of overlapping handles. * Fixes, flush overlapping texture data for ASTC and BC4/5 compressed textures. Fixes the new Life is Strange game. * Flush overlaps before init data, fix 3d texture size/overlap stuff * Fix 3D Textures, faster single layer flush Note: nosy people can no longer merge this with Vulkan. (unless they are nosy enough to implement the new backend methods) * Remove unused method * Minor cleanup * More cleanup * Use the More Fun and Hopefully No Driver Bugs method for getting compressed tex too This one's for metro * Address feedback, ASTC+ETC to FormatClass * Change offset to use Span slice rather than IntPtr Add * Fix this too --- Ryujinx.Graphics.Gpu/Image/Texture.cs | 358 +++++++++++++++++++--------------- 1 file changed, 204 insertions(+), 154 deletions(-) (limited to 'Ryujinx.Graphics.Gpu/Image/Texture.cs') diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs index 6d981479..ca43c430 100644 --- a/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -72,11 +72,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// public TextureGroup Group { get; private set; } - /// - /// Set when a texture has been modified by the Host GPU since it was last flushed. - /// - public bool IsModified { get; internal set; } - /// /// Set when a texture has been changed size. This indicates that it may need to be /// changed again when obtained as a sampler. @@ -89,6 +84,12 @@ namespace Ryujinx.Graphics.Gpu.Image /// public bool ChangedMapping { get; private set; } + /// + /// 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. + /// + public bool AlwaysFlushOnOverlap { get; private set; } + private int _depth; private int _layers; public int FirstLayer { get; private set; } @@ -99,6 +100,8 @@ namespace Ryujinx.Graphics.Gpu.Image private int _updateCount; private byte[] _currentData; + private bool _modifiedStale = true; + private ITexture _arrayViewTexture; private Target _arrayViewTarget; @@ -241,6 +244,8 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if the texture is to be initialized with data public void InitializeData(bool isView, bool withData = false) { + withData |= Group != null && Group.FlushIncompatibleOverlapsIfNeeded(); + if (withData) { Debug.Assert(!isView); @@ -280,9 +285,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// True if the texture will have layer views /// True if the texture will have mip views - public void InitializeGroup(bool hasLayerViews, bool hasMipViews) + /// Groups that overlap with this one but are incompatible + public void InitializeGroup(bool hasLayerViews, bool hasMipViews, List incompatibleOverlaps) { - Group = new TextureGroup(_context, _physicalMemory, this); + Group = new TextureGroup(_context, _physicalMemory, this, incompatibleOverlaps); Group.Initialize(ref _sizeInfo, hasLayerViews, hasMipViews); } @@ -657,6 +663,14 @@ namespace Ryujinx.Graphics.Gpu.Image _dirty = true; } + /// + /// Signal that the modified state is dirty, indicating that the texture group should be notified when it changes. + /// + public void SignalModifiedDirty() + { + _modifiedStale = true; + } + /// /// Fully synchronizes guest and host memory. /// This will replace the entire texture with the data present in guest memory. @@ -670,8 +684,6 @@ namespace Ryujinx.Graphics.Gpu.Image ReadOnlySpan data = _physicalMemory.GetSpan(Range); - IsModified = false; - // 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. @@ -710,7 +722,7 @@ namespace Ryujinx.Graphics.Gpu.Image Group.CheckDirty(this, true); - IsModified = false; + AlwaysFlushOnOverlap = true; HostTexture.SetData(data); @@ -738,15 +750,17 @@ namespace Ryujinx.Graphics.Gpu.Image /// Converts texture data to a format and layout that is supported by the host GPU. /// /// Data to be converted + /// Mip level to convert + /// True to convert a single slice /// Converted data public ReadOnlySpan ConvertToHostCompatibleFormat(ReadOnlySpan data, int level = 0, bool single = false) { int width = Info.Width; int height = Info.Height; - int depth = single ? 1 : _depth; + int depth = _depth; int layers = single ? 1 : _layers; - int levels = single ? 1 : Info.Levels; + int levels = single ? 1 : (Info.Levels - level); width = Math.Max(width >> level, 1); height = Math.Max(height >> level, 1); @@ -770,6 +784,7 @@ namespace Ryujinx.Graphics.Gpu.Image width, height, depth, + single ? 1 : depth, levels, layers, Info.FormatInfo.BlockWidth, @@ -821,6 +836,65 @@ namespace Ryujinx.Graphics.Gpu.Image return data; } + /// + /// Converts texture data from a format and layout that is supported by the host GPU, back into the intended format on the guest GPU. + /// + /// Optional output span to convert into + /// Data to be converted + /// Mip level to convert + /// True to convert a single slice + /// Converted data + public ReadOnlySpan ConvertFromHostCompatibleFormat(Span output, ReadOnlySpan 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; + } + /// /// Flushes the texture data. /// This causes the texture data to be written back to guest memory. @@ -830,56 +904,48 @@ namespace Ryujinx.Graphics.Gpu.Image /// This may cause data corruption if the memory is already being used for something else on the CPU side. /// /// 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) + /// True if data was flushed, false otherwise + public bool FlushModified(bool tracked = true) { - IsModified = false; - if (TextureCompatibility.IsFormatHostIncompatible(Info, _context.Capabilities)) - { - return; // Flushing this format is not supported, as it may have been converted to another host format. - } - - FlushTextureDataToGuest(tracked); + return TextureCompatibility.CanTextureFlush(Info, _context.Capabilities) && Group.FlushModified(this, tracked); } /// - /// Flushes the texture data, to be called from an external thread. - /// The host backend must ensure that we have shared access to the resource from this thread. - /// This is used when flushing from memory access handlers. + /// 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. /// - public void ExternalFlush(ulong address, ulong size) + /// 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) { - if (!IsModified) + if (TextureCompatibility.CanTextureFlush(Info, _context.Capabilities)) { - return; + FlushTextureDataToGuest(tracked); } - - _context.Renderer.BackgroundContextAction(() => + } + + /// + /// 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. + /// + /// The host texture to flush + public ITexture GetFlushTexture() + { + ITexture texture = HostTexture; + if (ScaleFactor != 1f) { - IsModified = false; - if (TextureCompatibility.IsFormatHostIncompatible(Info, _context.Capabilities)) - { - return; // 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; // Flushing multisample textures is not supported, the host does not allow getting their data. - } - - ITexture texture = HostTexture; - if (ScaleFactor != 1f) - { - // If needed, create a texture to flush back to host at 1x scale. - texture = _flushHostTexture = GetScaledHostTexture(1f, _flushHostTexture); - } + // If needed, create a texture to flush back to host at 1x scale. + texture = _flushHostTexture = GetScaledHostTexture(1f, _flushHostTexture); + } - FlushTextureDataToGuest(false, texture); - }); + return texture; } /// - /// Gets data from the host GPU, and flushes it to guest memory. + /// Gets data from the host GPU, and flushes it all to guest memory. /// /// /// This method should be used to retrieve data that was modified by the host GPU. @@ -888,28 +954,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// True if writing the texture data is tracked, false otherwise /// The specific host texture to flush. Defaults to this texture - private void FlushTextureDataToGuest(bool tracked, ITexture texture = null) + public void FlushTextureDataToGuest(bool tracked, ITexture texture = null) { - if (Range.Count == 1) - { - MemoryRange subrange = Range.GetSubRange(0); + using WritableRegion region = _physicalMemory.GetWritableRegion(Range, tracked); - using (WritableRegion region = _physicalMemory.GetWritableRegion(subrange.Address, (int)subrange.Size, tracked)) - { - GetTextureDataFromGpu(region.Memory.Span, tracked, texture); - } - } - else - { - if (tracked) - { - _physicalMemory.Write(Range, GetTextureDataFromGpu(Span.Empty, true, texture)); - } - else - { - _physicalMemory.WriteUntracked(Range, GetTextureDataFromGpu(Span.Empty, false, texture)); - } - } + GetTextureDataFromGpu(region.Memory.Span, tracked, texture); } /// @@ -951,40 +1000,54 @@ namespace Ryujinx.Graphics.Gpu.Image } } - if (Target != Target.TextureBuffer) + data = ConvertFromHostCompatibleFormat(output, data); + + return data; + } + + /// + /// Gets data from the host GPU for a single slice. + /// + /// + /// 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. + /// + /// An output span to place the texture data into. If empty, one is generated + /// The layer of the texture to flush + /// The level of the texture to flush + /// True if the texture should be blacklisted, false otherwise + /// The specific host texture to flush. Defaults to this texture + /// The span containing the texture data + public ReadOnlySpan GetTextureDataSliceFromGpu(Span output, int layer, int level, bool blacklist, ITexture texture = null) + { + ReadOnlySpan data; + + if (texture != null) { - if (Info.IsLinear) + data = texture.GetData(layer, level); + } + else + { + if (blacklist) { - data = LayoutConverter.ConvertLinearToLinearStrided( - output, - Info.Width, - Info.Height, - Info.FormatInfo.BlockWidth, - Info.FormatInfo.BlockHeight, - Info.Stride, - Info.FormatInfo.BytesPerPixel, - data); + BlacklistScale(); + data = HostTexture.GetData(layer, level); + } + else if (ScaleFactor != 1f) + { + float scale = ScaleFactor; + SetScale(1f); + data = HostTexture.GetData(layer, level); + SetScale(scale); } else { - data = LayoutConverter.ConvertLinearToBlockLinear( - output, - Info.Width, - Info.Height, - _depth, - Info.Levels, - _layers, - Info.FormatInfo.BlockWidth, - Info.FormatInfo.BlockHeight, - Info.FormatInfo.BytesPerPixel, - Info.GobBlocksInY, - Info.GobBlocksInZ, - Info.GobBlocksInTileX, - _sizeInfo, - data); + data = HostTexture.GetData(layer, level); } } + data = ConvertFromHostCompatibleFormat(output, data, level, true); + return data; } @@ -1043,55 +1106,64 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Texture view information /// Texture view physical memory ranges + /// Layer size on the given texture + /// Host GPU capabilities /// Texture view initial layer on this texture /// Texture view first mipmap level on this texture /// The level of compatiblilty a view with the given parameters created from this texture has - public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, int layerSize, out int firstLayer, out int firstLevel) + public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, int layerSize, Capabilities caps, out int firstLayer, out int firstLevel) { - int offset = Range.FindOffset(range); + TextureViewCompatibility result = TextureViewCompatibility.Full; - // Out of range. - if (offset < 0) + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewFormatCompatible(Info, info, caps)); + if (result != TextureViewCompatibility.Incompatible) { - firstLayer = 0; - firstLevel = 0; + result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info)); - return 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; + } + + if (Info.SamplesInX != info.SamplesInX || Info.SamplesInY != info.SamplesInY) + { + result = TextureViewCompatibility.Incompatible; + } } - if (!_sizeInfo.FindView(offset, out firstLayer, out firstLevel)) + 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.Incompatible; + return TextureViewCompatibility.LayoutIncompatible; } if (info.GetSlices() > 1 && LayerSize != layerSize) { - return TextureViewCompatibility.Incompatible; + return TextureViewCompatibility.LayoutIncompatible; } - TextureViewCompatibility result = TextureViewCompatibility.Full; - - result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewFormatCompatible(Info, info)); result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSizeMatches(Info, info, firstLevel)); - result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info)); result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSubImagesInBounds(Info, info, firstLayer, firstLevel)); - 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; - } - - return (Info.SamplesInX == info.SamplesInX && - Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible; + return result; } /// @@ -1261,11 +1333,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { - bool wasModified = IsModified; - if (!wasModified || Group.HasCopyDependencies) + if (_modifiedStale || Group.HasCopyDependencies) { - IsModified = true; - Group.SignalModified(this, !wasModified); + _modifiedStale = false; + Group.SignalModified(this); } _physicalMemory.TextureCache.Lift(this); @@ -1278,12 +1349,10 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if the texture has been bound, false if it has been unbound public void SignalModifying(bool bound) { - bool wasModified = IsModified; - - if (!wasModified || Group.HasCopyDependencies) + if (_modifiedStale || Group.HasCopyDependencies) { - IsModified = true; - Group.SignalModifying(this, bound, !wasModified); + _modifiedStale = false; + Group.SignalModifying(this, bound); } _physicalMemory.TextureCache.Lift(this); @@ -1309,29 +1378,6 @@ namespace Ryujinx.Graphics.Gpu.Image HostTexture = hostTexture; } - /// - /// 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.Range, view.LayerSize, out _, out _) != TextureViewCompatibility.Incompatible) - { - return true; - } - } - - return false; - } - /// /// Determine if any of this texture's data overlaps with another. /// @@ -1489,11 +1535,15 @@ namespace Ryujinx.Graphics.Gpu.Image /// Called when the memory for this texture has been unmapped. /// Calls are from non-gpu threads. /// - public void Unmapped() + /// The range of memory being unmapped + public void Unmapped(MultiRange unmapRange) { ChangedMapping = true; - IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped. + if (Group.Storage == this) + { + Group.ClearModified(unmapRange); + } RemoveFromPools(true); } -- cgit v1.2.3