diff --git a/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs b/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs index 9dbdbfcbc..f4391aad4 100644 --- a/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs +++ b/Ryujinx.Cpu/Tracking/CpuRegionHandle.cs @@ -1,4 +1,5 @@ using Ryujinx.Memory.Tracking; +using System; namespace Ryujinx.Cpu.Tracking { @@ -18,6 +19,9 @@ namespace Ryujinx.Cpu.Tracking public void Dispose() => _impl.Dispose(); public void RegisterAction(RegionSignal action) => _impl.RegisterAction(action); - public void Reprotect() => _impl.Reprotect(); + public void RegisterDirtyEvent(Action action) => _impl.RegisterDirtyEvent(action); + public void Reprotect(bool asDirty = false) => _impl.Reprotect(asDirty); + + public bool OverlapsWith(ulong address, ulong size) => _impl.OverlapsWith(address, size); } } diff --git a/Ryujinx.Graphics.GAL/ITexture.cs b/Ryujinx.Graphics.GAL/ITexture.cs index 543f9de08..ad8fd297c 100644 --- a/Ryujinx.Graphics.GAL/ITexture.cs +++ b/Ryujinx.Graphics.GAL/ITexture.cs @@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.GAL float ScaleFactor { get; } void CopyTo(ITexture destination, int firstLayer, int firstLevel); + void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel); void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter); ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel); @@ -16,6 +17,7 @@ namespace Ryujinx.Graphics.GAL byte[] GetData(); void SetData(ReadOnlySpan data); + void SetData(ReadOnlySpan data, int layer, int level); void SetStorage(BufferRange buffer); void Release(); } diff --git a/Ryujinx.Graphics.Gpu/Engine/Methods.cs b/Ryujinx.Graphics.Gpu/Engine/Methods.cs index a41fd5414..0731f1c2b 100644 --- a/Ryujinx.Graphics.Gpu/Engine/Methods.cs +++ b/Ryujinx.Graphics.Gpu/Engine/Methods.cs @@ -377,11 +377,6 @@ namespace Ryujinx.Graphics.Gpu.Engine Texture color = TextureManager.FindOrCreateTexture(colorState, samplesInX, samplesInY, sizeHint); changedScale |= TextureManager.SetRenderTargetColor(index, color); - - if (color != null) - { - color.SignalModified(); - } } bool dsEnable = state.Get(MethodOffset.RtDepthStencilEnable); @@ -406,11 +401,6 @@ namespace Ryujinx.Graphics.Gpu.Engine UpdateViewportTransform(state); UpdateScissorState(state); } - - if (depthStencil != null) - { - depthStencil.SignalModified(); - } } /// diff --git a/Ryujinx.Graphics.Gpu/Image/Texture.cs b/Ryujinx.Graphics.Gpu/Image/Texture.cs index 6dfe46288..4d4091cb1 100644 --- a/Ryujinx.Graphics.Gpu/Image/Texture.cs +++ b/Ryujinx.Graphics.Gpu/Image/Texture.cs @@ -50,6 +50,11 @@ namespace Ryujinx.Graphics.Gpu.Image /// public TextureScaleMode ScaleMode { get; private set; } + /// + /// Group that this texture belongs to. Manages read/write memory tracking. + /// + public TextureGroup Group { get; private set; } + /// /// Set when a texture has been modified by the Host GPU since it was last flushed. /// @@ -63,10 +68,11 @@ namespace Ryujinx.Graphics.Gpu.Image private int _depth; private int _layers; - private int _firstLayer; - private int _firstLevel; + public int FirstLayer { get; private set; } + public int FirstLevel { get; private set; } private bool _hasData; + private bool _dirty = true; private int _updateCount; private byte[] _currentData; @@ -99,12 +105,20 @@ namespace Ryujinx.Graphics.Gpu.Image /// public MultiRange Range { get; private set; } + /// + /// Layer size in bytes. + /// + public int LayerSize => _sizeInfo.LayerSize; + /// /// Texture size in bytes. /// public ulong Size => (ulong)_sizeInfo.TotalSize; - private GpuRegionHandle _memoryTracking; + /// + /// Whether or not the texture belongs is a view. + /// + public bool IsView => _viewStorage != this; private int _referenceCount; @@ -131,8 +145,8 @@ namespace Ryujinx.Graphics.Gpu.Image { InitializeTexture(context, info, sizeInfo, range); - _firstLayer = firstLayer; - _firstLevel = firstLevel; + FirstLayer = firstLayer; + FirstLevel = firstLevel; ScaleFactor = scaleFactor; ScaleMode = scaleMode; @@ -186,8 +200,6 @@ namespace Ryujinx.Graphics.Gpu.Image /// True if the texture is to be initialized with data public void InitializeData(bool isView, bool withData = false) { - _memoryTracking = _context.PhysicalMemory.BeginTracking(Range); - if (withData) { Debug.Assert(!isView); @@ -203,12 +215,13 @@ namespace Ryujinx.Graphics.Gpu.Image } else { - // Don't update this texture the next time we synchronize. - ConsumeModified(); _hasData = true; if (!isView) { + // Don't update this texture the next time we synchronize. + ConsumeModified(); + 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. @@ -221,6 +234,18 @@ namespace Ryujinx.Graphics.Gpu.Image } } + /// + /// Initialize a new texture group with this texture as storage. + /// + /// True if the texture will have layer views + /// True if the texture will have mip views + public void InitializeGroup(bool hasLayerViews, bool hasMipViews) + { + Group = new TextureGroup(_context, this); + + Group.Initialize(ref _sizeInfo, hasLayerViews, hasMipViews); + } + /// /// Create a texture view from this texture. /// A texture view is defined as a child texture, from a sub-range of their parent texture. @@ -240,8 +265,8 @@ namespace Ryujinx.Graphics.Gpu.Image info, sizeInfo, range, - _firstLayer + firstLayer, - _firstLevel + firstLevel, + FirstLayer + firstLayer, + FirstLevel + firstLevel, ScaleFactor, ScaleMode); @@ -259,11 +284,26 @@ namespace Ryujinx.Graphics.Gpu.Image /// The child texture private void AddView(Texture texture) { - DisableMemoryTracking(); + IncrementReferenceCount(); _views.Add(texture); texture._viewStorage = this; + + Group.UpdateViews(_views); + + if (texture.Group != null && texture.Group != Group) + { + if (texture.Group.Storage == texture) + { + // This texture's group is no longer used. + Group.Inherit(texture.Group); + + texture.Group.Dispose(); + } + } + + texture.Group = Group; } /// @@ -276,7 +316,27 @@ namespace Ryujinx.Graphics.Gpu.Image texture._viewStorage = texture; - DeleteIfNotUsed(); + DecrementReferenceCount(); + } + + /// + /// Create a copy dependency to a texture that is view compatible with this one. + /// When either texture is modified, the texture data will be copied to the other to keep them in sync. + /// This is essentially an emulated view, useful for handling multiple view parents or format incompatibility. + /// This also forces a copy on creation, to or from the given texture to get them in sync immediately. + /// + /// The view compatible texture to create a dependency to + /// The base layer of the given texture relative to this one + /// The base level of the given texture relative to this one + /// True if this texture is first copied to the given one, false for the opposite direction + public void CreateCopyDependency(Texture contained, int layer, int level, bool copyTo) + { + if (contained.Group == Group) + { + return; + } + + Group.CreateCopyDependency(contained, FirstLayer + layer, FirstLevel + level, copyTo); } /// @@ -294,12 +354,12 @@ namespace Ryujinx.Graphics.Gpu.Image int blockWidth = Info.FormatInfo.BlockWidth; int blockHeight = Info.FormatInfo.BlockHeight; - width <<= _firstLevel; - height <<= _firstLevel; + width <<= FirstLevel; + height <<= FirstLevel; if (Target == Target.Texture3D) { - depthOrLayers <<= _firstLevel; + depthOrLayers <<= FirstLevel; } else { @@ -310,14 +370,14 @@ namespace Ryujinx.Graphics.Gpu.Image foreach (Texture view in _viewStorage._views) { - int viewWidth = Math.Max(1, width >> view._firstLevel); - int viewHeight = Math.Max(1, height >> view._firstLevel); + int viewWidth = Math.Max(1, width >> view.FirstLevel); + int viewHeight = Math.Max(1, height >> view.FirstLevel); int viewDepthOrLayers; if (view.Info.Target == Target.Texture3D) { - viewDepthOrLayers = Math.Max(1, depthOrLayers >> view._firstLevel); + viewDepthOrLayers = Math.Max(1, depthOrLayers >> view.FirstLevel); } else { @@ -328,16 +388,6 @@ namespace Ryujinx.Graphics.Gpu.Image } } - /// - /// Disables memory tracking on this texture. Currently used for view containers, as we assume their views are covering all memory regions. - /// Textures with disabled memory tracking also cannot flush in most circumstances. - /// - public void DisableMemoryTracking() - { - _memoryTracking?.Dispose(); - _memoryTracking = null; - } - /// /// Recreates the texture storage (or view, in the case of child textures) of this texture. /// This allows recreating the texture with a new size. @@ -393,7 +443,7 @@ namespace Ryujinx.Graphics.Gpu.Image if (_viewStorage != this) { - ReplaceStorage(_viewStorage.HostTexture.CreateView(createInfo, _firstLayer, _firstLevel)); + ReplaceStorage(_viewStorage.HostTexture.CreateView(createInfo, FirstLayer, FirstLevel)); } else { @@ -495,7 +545,7 @@ namespace Ryujinx.Graphics.Gpu.Image view.ScaleFactor = scale; TextureCreateInfo viewCreateInfo = TextureManager.GetCreateInfo(view.Info, _context.Capabilities, scale); - ITexture newView = HostTexture.CreateView(viewCreateInfo, view._firstLayer - _firstLayer, view._firstLevel - _firstLevel); + ITexture newView = HostTexture.CreateView(viewCreateInfo, view.FirstLayer - FirstLayer, view.FirstLevel - FirstLevel); view.ReplaceStorage(newView); view.ScaleMode = newScaleMode; @@ -517,17 +567,10 @@ 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. /// - /// - /// If there is no memory tracking for this texture, it will always report as modified. - /// /// True if the texture was modified, false otherwise. public bool ConsumeModified() { - bool wasDirty = _memoryTracking?.Dirty ?? true; - - _memoryTracking?.Reprotect(); - - return wasDirty; + return Group.ConsumeDirty(this); } /// @@ -544,17 +587,42 @@ namespace Ryujinx.Graphics.Gpu.Image return; } - if (_hasData) + if (!_dirty) { - if (_memoryTracking?.Dirty != true) - { - return; - } - - BlacklistScale(); + return; } - _memoryTracking?.Reprotect(); + _dirty = false; + + if (_hasData) + { + Group.SynchronizeMemory(this); + } + else + { + Group.ConsumeDirty(this); + SynchronizeFull(); + } + } + + /// + /// Signal that this texture is dirty, indicating that the texture group must be checked. + /// + public void SignalGroupDirty() + { + _dirty = true; + } + + /// + /// Fully synchronizes guest and host memory. + /// This will replace the entire texture with the data present in guest memory. + /// + public void SynchronizeFull() + { + if (_hasData) + { + BlacklistScale(); + } ReadOnlySpan data = _context.PhysicalMemory.GetSpan(Range); @@ -596,7 +664,7 @@ namespace Ryujinx.Graphics.Gpu.Image { BlacklistScale(); - _memoryTracking?.Reprotect(); + Group.ConsumeDirty(this); IsModified = false; @@ -605,18 +673,46 @@ namespace Ryujinx.Graphics.Gpu.Image _hasData = true; } + /// + /// Uploads new texture data to the host GPU for a specific layer/level. + /// + /// New data + /// Target layer + /// Target level + public void SetData(ReadOnlySpan data, int layer, int level) + { + BlacklistScale(); + + HostTexture.SetData(data, layer, level); + + _currentData = null; + + _hasData = true; + } + /// /// Converts texture data to a format and layout that is supported by the host GPU. /// /// Data to be converted /// Converted data - private ReadOnlySpan ConvertToHostCompatibleFormat(ReadOnlySpan 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 layers = single ? 1 : _layers; + int levels = single ? 1 : Info.Levels; + + width = Math.Max(width >> level, 1); + height = Math.Max(height >> level, 1); + depth = Math.Max(depth >> level, 1); + if (Info.IsLinear) { data = LayoutConverter.ConvertLinearStridedToLinear( - Info.Width, - Info.Height, + width, + height, Info.FormatInfo.BlockWidth, Info.FormatInfo.BlockHeight, Info.Stride, @@ -626,11 +722,11 @@ namespace Ryujinx.Graphics.Gpu.Image else { data = LayoutConverter.ConvertBlockLinearToLinear( - Info.Width, - Info.Height, - _depth, - Info.Levels, - _layers, + width, + height, + depth, + levels, + layers, Info.FormatInfo.BlockWidth, Info.FormatInfo.BlockHeight, Info.FormatInfo.BytesPerPixel, @@ -650,11 +746,11 @@ namespace Ryujinx.Graphics.Gpu.Image data.ToArray(), Info.FormatInfo.BlockWidth, Info.FormatInfo.BlockHeight, - Info.Width, - Info.Height, - _depth, - Info.Levels, - _layers, + width, + height, + depth, + levels, + layers, out Span decoded)) { string texInfo = $"{Info.Target} {Info.FormatInfo.Format} {Info.Width}x{Info.Height}x{Info.DepthOrLayers} levels {Info.Levels}"; @@ -666,11 +762,11 @@ namespace Ryujinx.Graphics.Gpu.Image } else if (Target == Target.Texture3D && Info.FormatInfo.Format.IsBc4()) { - data = BCnDecoder.DecodeBC4(data, Info.Width, Info.Height, _depth, Info.Levels, _layers, Info.FormatInfo.Format == Format.Bc4Snorm); + data = BCnDecoder.DecodeBC4(data, width, height, depth, levels, layers, Info.FormatInfo.Format == Format.Bc4Snorm); } else if (Target == Target.Texture3D && Info.FormatInfo.Format.IsBc5()) { - data = BCnDecoder.DecodeBC5(data, Info.Width, Info.Height, _depth, Info.Levels, _layers, Info.FormatInfo.Format == Format.Bc5Snorm); + data = BCnDecoder.DecodeBC5(data, width, height, depth, levels, layers, Info.FormatInfo.Format == Format.Bc5Snorm); } return data; @@ -710,7 +806,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void ExternalFlush(ulong address, ulong size) { - if (!IsModified || _memoryTracking == null) + if (!IsModified) { return; } @@ -869,7 +965,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// 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, out int firstLayer, out int firstLevel) + public TextureViewCompatibility IsViewCompatible(TextureInfo info, MultiRange range, int layerSize, out int firstLayer, out int firstLevel) { int offset = Range.FindOffset(range); @@ -892,15 +988,17 @@ namespace Ryujinx.Graphics.Gpu.Image return TextureViewCompatibility.Incompatible; } - if (!TextureCompatibility.ViewFormatCompatible(Info, info)) + if (info.GetSlices() > 1 && LayerSize != layerSize) { return TextureViewCompatibility.Incompatible; } 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)); return (Info.SamplesInX == info.SamplesInX && Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible; @@ -1003,14 +1101,37 @@ namespace Ryujinx.Graphics.Gpu.Image /// The first level of the view public void ReplaceView(Texture parent, TextureInfo info, ITexture hostTexture, int firstLayer, int firstLevel) { + IncrementReferenceCount(); parent._viewStorage.SynchronizeMemory(); + + // If this texture has views, they must be given to the new parent. + if (_views.Count > 0) + { + Texture[] viewCopy = _views.ToArray(); + + foreach (Texture view in viewCopy) + { + TextureCreateInfo createInfo = TextureManager.GetCreateInfo(view.Info, _context.Capabilities, ScaleFactor); + + ITexture newView = parent.HostTexture.CreateView(createInfo, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel); + + view.ReplaceView(parent, view.Info, newView, view.FirstLayer + firstLayer, view.FirstLevel + firstLevel); + } + } + ReplaceStorage(hostTexture); - _firstLayer = parent._firstLayer + firstLayer; - _firstLevel = parent._firstLevel + firstLevel; + if (_viewStorage != this) + { + _viewStorage.RemoveView(this); + } + + FirstLayer = parent.FirstLayer + firstLayer; + FirstLevel = parent.FirstLevel + firstLevel; parent._viewStorage.AddView(this); SetInfo(info); + DecrementReferenceCount(); } /// @@ -1031,14 +1152,28 @@ namespace Ryujinx.Graphics.Gpu.Image /// public void SignalModified() { - IsModified = true; - - if (_viewStorage != this) + bool wasModified = IsModified; + if (!wasModified || Group.HasCopyDependencies) { - _viewStorage.SignalModified(); + IsModified = true; + Group.SignalModified(this, !wasModified); } + } - _memoryTracking?.RegisterAction(ExternalFlush); + /// + /// Signals that a texture has been bound, or has been unbound. + /// During this time, lazy copies will not clear the dirty flag. + /// + /// 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) + { + IsModified = true; + Group.SignalModifying(this, bound, !wasModified); + } } /// @@ -1066,7 +1201,7 @@ namespace Ryujinx.Graphics.Gpu.Image foreach (Texture view in _views) { - if (texture.IsViewCompatible(view.Info, view.Range, out _, out _) != TextureViewCompatibility.Incompatible) + if (texture.IsViewCompatible(view.Info, view.Range, view.LayerSize, out _, out _) != TextureViewCompatibility.Incompatible) { return true; } @@ -1148,10 +1283,6 @@ namespace Ryujinx.Graphics.Gpu.Image public void Unmapped() { IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped. - - var tracking = _memoryTracking; - tracking?.Reprotect(); - tracking?.RegisterAction(null); } /// @@ -1162,7 +1293,11 @@ namespace Ryujinx.Graphics.Gpu.Image DisposeTextures(); Disposed?.Invoke(this); - _memoryTracking?.Dispose(); + + if (Group.Storage == this) + { + Group.Dispose(); + } } } } \ No newline at end of file diff --git a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs index e3574be5d..d613612ff 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureCompatibility.cs @@ -215,7 +215,7 @@ namespace Ryujinx.Graphics.Gpu.Image /// 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 if the sizes are compatible, false otherwise + /// The view compatibility level of the view sizes public static TextureViewCompatibility ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level) { Size size = GetAlignedSize(lhs, level); @@ -235,6 +235,27 @@ namespace Ryujinx.Graphics.Gpu.Image size.Height == otherSize.Height) ? result : TextureViewCompatibility.Incompatible; } + /// + /// Checks if the potential child texture fits within the level and layer bounds of the parent. + /// + /// Texture information for the parent + /// Texture information for the child + /// Base layer of the child texture + /// Base level of the child texture + /// Full compatiblity if the child's layer and level count fit within the parent, incompatible otherwise + public static TextureViewCompatibility ViewSubImagesInBounds(TextureInfo parent, TextureInfo child, int layer, int level) + { + if (level + child.Levels <= parent.Levels && + layer + child.GetSlices() <= parent.GetSlices()) + { + return TextureViewCompatibility.Full; + } + else + { + return TextureViewCompatibility.Incompatible; + } + } + /// /// Checks if the texture sizes of the supplied texture informations match. /// @@ -382,10 +403,22 @@ namespace Ryujinx.Graphics.Gpu.Image /// /// Texture information of the texture view /// Texture information of the texture view - /// True if the formats are compatible, false otherwise - public static bool ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs) + /// The view compatibility level of the texture formats + public static TextureViewCompatibility ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs) { - return FormatCompatible(lhs.FormatInfo, rhs.FormatInfo); + if (FormatCompatible(lhs.FormatInfo, rhs.FormatInfo)) + { + if (lhs.FormatInfo.IsCompressed != rhs.FormatInfo.IsCompressed) + { + return TextureViewCompatibility.CopyOnly; + } + else + { + return TextureViewCompatibility.Full; + } + } + + return TextureViewCompatibility.Incompatible; } /// diff --git a/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs b/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs new file mode 100644 index 000000000..269ddbd97 --- /dev/null +++ b/Ryujinx.Graphics.Gpu/Image/TextureDependency.cs @@ -0,0 +1,37 @@ +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// One side of a two-way dependency between one texture view and another. + /// Contains a reference to the handle owning the dependency, and the other dependency. + /// + class TextureDependency + { + /// + /// The handle that owns this dependency. + /// + public TextureGroupHandle Handle; + + /// + /// The other dependency linked to this one, which belongs to another handle. + /// + public TextureDependency Other; + + /// + /// Create a new texture dependency. + /// + /// The handle that owns the dependency + public TextureDependency(TextureGroupHandle handle) + { + Handle = handle; + } + + /// + /// Signal that the owner of this dependency has been modified, + /// meaning that the other dependency's handle must defer a copy from it. + /// + public void SignalModified() + { + Other.Handle.DeferCopy(Handle); + } + } +} diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs new file mode 100644 index 000000000..5d150559f --- /dev/null +++ b/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs @@ -0,0 +1,971 @@ +using Ryujinx.Common; +using Ryujinx.Cpu.Tracking; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Texture; +using Ryujinx.Memory.Range; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// A texture group represents a group of textures that belong to the same storage. + /// When views are created, this class will track memory accesses for them separately. + /// The group iteratively adds more granular tracking as views of different kinds are added. + /// Note that a texture group can be absorbed into another when it becomes a view parent. + /// + class TextureGroup : IDisposable + { + private const int StrideAlignment = 32; + private const int GobAlignment = 64; + + private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false); + + /// + /// The storage texture associated with this group. + /// + public Texture Storage { get; } + + /// + /// Indicates if the texture has copy dependencies. If true, then all modifications + /// must be signalled to the group, rather than skipping ones still to be flushed. + /// + public bool HasCopyDependencies { get; set; } + + private GpuContext _context; + + private int[] _allOffsets; + private int[] _sliceSizes; + private bool _is3D; + private bool _hasMipViews; + private bool _hasLayerViews; + private int _layers; + private int _levels; + + private MultiRange TextureRange => Storage.Range; + + /// + /// The views list from the storage texture. + /// + private List _views; + private TextureGroupHandle[] _handles; + private bool[] _loadNeeded; + + /// + /// Create a new texture group. + /// + /// GPU context that the texture group belongs to + /// The storage texture for this group + public TextureGroup(GpuContext context, Texture storage) + { + Storage = storage; + _context = context; + + _is3D = storage.Info.Target == Target.Texture3D; + _layers = storage.Info.GetSlices(); + _levels = storage.Info.Levels; + } + + /// + /// Initialize a new texture group's dirty regions and offsets. + /// + /// Size info for the storage texture + /// True if the storage will have layer views + /// True if the storage will have mip views + public void Initialize(ref SizeInfo size, bool hasLayerViews, bool hasMipViews) + { + _allOffsets = size.AllOffsets; + _sliceSizes = size.SliceSizes; + + (_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews); + + RecalculateHandleRegions(); + } + + /// + /// Consume the dirty flags for a given texture. The state is shared between views of the same layers and levels. + /// + /// The texture being used + /// True if a flag was dirty, false otherwise + public bool ConsumeDirty(Texture texture) + { + bool dirty = false; + + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + foreach (CpuRegionHandle handle in group.Handles) + { + if (handle.Dirty) + { + handle.Reprotect(); + dirty = true; + } + } + } + }); + + return dirty; + } + + /// + /// Synchronize memory for a given texture. + /// If overlapping tracking handles are dirty, fully or partially synchronize the texture data. + /// + /// The texture being used + public void SynchronizeMemory(Texture texture) + { + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + bool dirty = false; + bool anyModified = false; + + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + bool modified = group.Modified; + bool handleDirty = false; + bool handleModified = false; + + foreach (CpuRegionHandle handle in group.Handles) + { + if (handle.Dirty) + { + handle.Reprotect(); + handleDirty = true; + } + else + { + handleModified |= modified; + } + } + + // Evaluate if any copy dependencies need to be fulfilled. A few rules: + // If the copy handle needs to be synchronized, prefer our own state. + // If we need to be synchronized and there is a copy present, prefer the copy. + + if (group.NeedsCopy && group.Copy()) + { + anyModified |= true; // The copy target has been modified. + handleDirty = false; + } + else + { + anyModified |= handleModified; + dirty |= handleDirty; + } + + if (group.NeedsCopy) + { + // The texture we copied from is still being written to. Copy from it again the next time this texture is used. + texture.SignalGroupDirty(); + } + + _loadNeeded[baseHandle + i] = handleDirty; + } + + if (dirty) + { + if (_handles.Length > 1 && (anyModified || split)) + { + // Partial texture invalidation. Only update the layers/levels with dirty flags of the storage. + + SynchronizePartial(baseHandle, regionCount); + } + else + { + // Full texture invalidation. + + texture.SynchronizeFull(); + } + } + }); + } + + /// + /// Synchronize part of the storage texture, represented by a given range of handles. + /// Only handles marked by the _loadNeeded array will be synchronized. + /// + /// The base index of the range of handles + /// The number of handles to synchronize + private void SynchronizePartial(int baseHandle, int regionCount) + { + ReadOnlySpan fullData = _context.PhysicalMemory.GetSpan(Storage.Range); + + for (int i = 0; i < regionCount; i++) + { + if (_loadNeeded[baseHandle + i]) + { + var info = GetHandleInformation(baseHandle + i); + int offsetIndex = info.Index; + + // Only one of these will be greater than 1, as partial sync is only called when there are sub-image views. + for (int layer = 0; layer < info.Layers; layer++) + { + for (int level = 0; level < info.Levels; level++) + { + int offset = _allOffsets[offsetIndex]; + int endOffset = (offsetIndex + 1 == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[offsetIndex + 1]; + int size = endOffset - offset; + + ReadOnlySpan data = fullData.Slice(offset, size); + + data = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel, true); + + Storage.SetData(data, info.BaseLayer, info.BaseLevel); + + offsetIndex++; + } + } + } + } + } + + /// + /// Signal that a texture in the group has been modified by the GPU. + /// + /// The texture that has been modified + /// True if the flushing read action should be registered, false otherwise + public void SignalModified(Texture texture, bool registerAction) + { + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + group.SignalModified(); + + if (registerAction) + { + RegisterAction(group); + } + } + }); + } + + /// + /// Signal that a texture in the group is actively bound, or has been unbound by the GPU. + /// + /// The texture that has been modified + /// True if this texture is being bound, false if unbound + /// True if the flushing read action should be registered, false otherwise + public void SignalModifying(Texture texture, bool bound, bool registerAction) + { + EvaluateRelevantHandles(texture, (baseHandle, regionCount, split) => + { + for (int i = 0; i < regionCount; i++) + { + TextureGroupHandle group = _handles[baseHandle + i]; + + group.SignalModifying(bound); + + if (registerAction) + { + RegisterAction(group); + } + } + }); + } + + /// + /// Register a read/write action to flush for a texture group. + /// + /// The group to register an action for + public void RegisterAction(TextureGroupHandle group) + { + foreach (CpuRegionHandle handle in group.Handles) + { + handle.RegisterAction((address, size) => FlushAction(group, address, size)); + } + } + + /// + /// Propagates the mip/layer view flags depending on the texture type. + /// When the most granular type of subresource has views, the other type of subresource must be segmented granularly too. + /// + /// True if the storage has layer views + /// True if the storage has mip views + /// The input values after propagation + private (bool HasLayerViews, bool HasMipViews) PropagateGranularity(bool hasLayerViews, bool hasMipViews) + { + if (_is3D) + { + hasMipViews |= hasLayerViews; + } + else + { + hasLayerViews |= hasMipViews; + } + + return (hasLayerViews, hasMipViews); + } + + /// + /// Evaluate the range of tracking handles which a view texture overlaps with. + /// + /// The texture to get handles for + /// + /// A function to be called with the base index of the range of handles for the given texture, and the number of handles it covers. + /// This can be called for multiple disjoint ranges, if required. + /// + private void EvaluateRelevantHandles(Texture texture, HandlesCallbackDelegate callback) + { + if (texture == Storage || !(_hasMipViews || _hasLayerViews)) + { + callback(0, _handles.Length); + + return; + } + + EvaluateRelevantHandles(texture.FirstLayer, texture.FirstLevel, texture.Info.GetSlices(), texture.Info.Levels, callback); + } + + /// + /// Evaluate the range of tracking handles which a view texture overlaps with, + /// using the view's position and slice/level counts. + /// + /// The first layer of the texture + /// The first level of the texture + /// The slice count of the texture + /// The level count of the texture + /// + /// A function to be called with the base index of the range of handles for the given texture, and the number of handles it covers. + /// This can be called for multiple disjoint ranges, if required. + /// + private void EvaluateRelevantHandles(int firstLayer, int firstLevel, int slices, int levels, HandlesCallbackDelegate callback) + { + int targetLayerHandles = _hasLayerViews ? slices : 1; + int targetLevelHandles = _hasMipViews ? levels : 1; + + if (_is3D) + { + // Future mip levels come after all layers of the last mip level. Each mipmap has less layers (depth) than the last. + + if (!_hasLayerViews) + { + // When there are no layer views, the mips are at a consistent offset. + + callback(firstLevel, targetLevelHandles); + } + else + { + (int levelIndex, int layerCount) = Get3DLevelRange(firstLevel); + + if (levels > 1 && slices < _layers) + { + // The given texture only covers some of the depth of multiple mips. (a "depth slice") + // Callback with each mip's range separately. + // Can assume that the group is fully subdivided (both slices and levels > 1 for storage) + + while (levels-- > 1) + { + callback(firstLayer + levelIndex, slices); + + levelIndex += layerCount; + layerCount = Math.Max(layerCount >> 1, 1); + slices = Math.Max(layerCount >> 1, 1); + } + } + else + { + int totalSize = Math.Min(layerCount, slices); + + while (levels-- > 1) + { + layerCount = Math.Max(layerCount >> 1, 1); + totalSize += layerCount; + } + + callback(firstLayer + levelIndex, totalSize); + } + } + } + else + { + // Future layers come after all mipmaps of the last. + int levelHandles = _hasMipViews ? _levels : 1; + + if (slices > 1 && levels < _levels) + { + // The given texture only covers some of the mipmaps of multiple slices. (a "mip slice") + // Callback with each layer's range separately. + // Can assume that the group is fully subdivided (both slices and levels > 1 for storage) + + for (int i = 0; i < slices; i++) + { + callback(firstLevel + (firstLayer + i) * levelHandles, targetLevelHandles, true); + } + } + else + { + callback(firstLevel + firstLayer * levelHandles, targetLevelHandles + (targetLayerHandles - 1) * levelHandles); + } + } + } + + /// + /// Get the range of offsets for a given mip level of a 3D texture. + /// + /// The level to return + /// Start index and count of offsets for the given level + private (int Index, int Count) Get3DLevelRange(int level) + { + int index = 0; + int count = _layers; // Depth. Halves with each mip level. + + while (level-- > 0) + { + index += count; + count = Math.Max(count >> 1, 1); + } + + return (index, count); + } + + /// + /// Get view information for a single tracking handle. + /// + /// The index of the handle + /// The layers and levels that the handle covers, and its index in the offsets array + private (int BaseLayer, int BaseLevel, int Levels, int Layers, int Index) GetHandleInformation(int handleIndex) + { + int baseLayer; + int baseLevel; + int levels = _hasMipViews ? 1 : _levels; + int layers = _hasLayerViews ? 1 : _layers; + int index; + + if (_is3D) + { + if (_hasLayerViews) + { + // NOTE: Will also have mip views, or only one level in storage. + + index = handleIndex; + baseLevel = 0; + + int layerLevels = _levels; + + while (handleIndex >= layerLevels) + { + handleIndex -= layerLevels; + baseLevel++; + layerLevels = Math.Max(layerLevels >> 1, 1); + } + + baseLayer = handleIndex; + } + else + { + baseLayer = 0; + baseLevel = handleIndex; + + (index, _) = Get3DLevelRange(baseLevel); + } + } + else + { + baseLevel = _hasMipViews ? handleIndex % _levels : 0; + baseLayer = _hasMipViews ? handleIndex / _levels : handleIndex; + index = baseLevel + baseLayer * _levels; + } + + return (baseLayer, baseLevel, levels, layers, index); + } + + /// + /// Gets the layer and level for a given view. + /// + /// The index of the view + /// The layer and level of the specified view + private (int BaseLayer, int BaseLevel) GetLayerLevelForView(int index) + { + if (_is3D) + { + int baseLevel = 0; + + int layerLevels = _layers; + + while (index >= layerLevels) + { + index -= layerLevels; + baseLevel++; + layerLevels = Math.Max(layerLevels >> 1, 1); + } + + return (index, baseLevel); + } + else + { + return (index / _levels, index % _levels); + } + } + + /// + /// Find the byte offset of a given texture relative to the storage. + /// + /// The texture to locate + /// The offset of the texture in bytes + public int FindOffset(Texture texture) + { + return _allOffsets[GetOffsetIndex(texture.FirstLayer, texture.FirstLevel)]; + } + + /// + /// Find the offset index of a given layer and level. + /// + /// The view layer + /// The view level + /// The offset index of the given layer and level + public int GetOffsetIndex(int layer, int level) + { + if (_is3D) + { + return layer + Get3DLevelRange(level).Index; + } + else + { + return level + layer * _levels; + } + } + + /// + /// The action to perform when a memory tracking handle is flipped to dirty. + /// This notifies overlapping textures that the memory needs to be synchronized. + /// + /// The handle that a dirty flag was set on + private void DirtyAction(TextureGroupHandle groupHandle) + { + // Notify all textures that belong to this handle. + + Storage.SignalGroupDirty(); + + lock (groupHandle.Overlaps) + { + foreach (Texture overlap in groupHandle.Overlaps) + { + overlap.SignalGroupDirty(); + } + } + } + + /// + /// Generate a CpuRegionHandle for a given address and size range in CPU VA. + /// + /// The start address of the tracked region + /// The size of the tracked region + /// A CpuRegionHandle covering the given range + private CpuRegionHandle GenerateHandle(ulong address, ulong size) + { + return _context.PhysicalMemory.BeginTracking(address, size); + } + + /// + /// Generate a TextureGroupHandle covering a specified range of views. + /// + /// The start view of the handle + /// The number of views to cover + /// A TextureGroupHandle covering the given views + private TextureGroupHandle GenerateHandles(int viewStart, int views) + { + int offset = _allOffsets[viewStart]; + int endOffset = (viewStart + views == _allOffsets.Length) ? (int)Storage.Size : _allOffsets[viewStart + views]; + int size = endOffset - offset; + + var result = new List(); + + for (int i = 0; i < TextureRange.Count; i++) + { + MemoryRange item = TextureRange.GetSubRange(i); + int subRangeSize = (int)item.Size; + + int sliceStart = Math.Clamp(offset, 0, subRangeSize); + int sliceEnd = Math.Clamp(endOffset, 0, subRangeSize); + + if (sliceStart != sliceEnd) + { + result.Add(GenerateHandle(item.Address + (ulong)sliceStart, (ulong)(sliceEnd - sliceStart))); + } + + offset -= subRangeSize; + endOffset -= subRangeSize; + + if (endOffset <= 0) + { + break; + } + } + + (int firstLayer, int firstLevel) = GetLayerLevelForView(viewStart); + + if (_hasLayerViews && _hasMipViews) + { + size = _sliceSizes[firstLevel]; + } + + var groupHandle = new TextureGroupHandle(this, _allOffsets[viewStart], (ulong)size, _views, firstLayer, firstLevel, result.ToArray()); + + foreach (CpuRegionHandle handle in result) + { + handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); + } + + return groupHandle; + } + + /// + /// Update the views in this texture group, rebuilding the memory tracking if required. + /// + /// The views list of the storage texture + public void UpdateViews(List views) + { + // This is saved to calculate overlapping views for each handle. + _views = views; + + bool layerViews = _hasLayerViews; + bool mipViews = _hasMipViews; + bool regionsRebuilt = false; + + if (!(layerViews && mipViews)) + { + foreach (Texture view in views) + { + if (view.Info.GetSlices() < _layers) + { + layerViews = true; + } + + if (view.Info.Levels < _levels) + { + mipViews = true; + } + } + + (layerViews, mipViews) = PropagateGranularity(layerViews, mipViews); + + if (layerViews != _hasLayerViews || mipViews != _hasMipViews) + { + _hasLayerViews = layerViews; + _hasMipViews = mipViews; + + RecalculateHandleRegions(); + regionsRebuilt = true; + } + } + + if (!regionsRebuilt) + { + // Must update the overlapping views on all handles, but only if they were not just recreated. + + foreach (TextureGroupHandle handle in _handles) + { + handle.RecalculateOverlaps(this, views); + } + } + + Storage.SignalGroupDirty(); + foreach (Texture texture in views) + { + texture.SignalGroupDirty(); + } + } + + /// + /// Inherit handle state from an old set of handles, such as modified and dirty flags. + /// + /// The set of handles to inherit state from + /// The set of handles inheriting the state + private void InheritHandles(TextureGroupHandle[] oldHandles, TextureGroupHandle[] handles) + { + foreach (var group in handles) + { + foreach (var handle in group.Handles) + { + bool dirty = false; + + foreach (var oldGroup in oldHandles) + { + if (group.OverlapsWith(oldGroup.Offset, oldGroup.Size)) + { + foreach (var oldHandle in oldGroup.Handles) + { + if (handle.OverlapsWith(oldHandle.Address, oldHandle.Size)) + { + dirty |= oldHandle.Dirty; + } + } + + group.Inherit(oldGroup); + } + } + + if (dirty && !handle.Dirty) + { + handle.Reprotect(true); + } + + if (group.Modified) + { + handle.RegisterAction((address, size) => FlushAction(group, address, size)); + } + } + } + } + + /// + /// Inherit state from another texture group. + /// + /// The texture group to inherit from + public void Inherit(TextureGroup other) + { + bool layerViews = _hasLayerViews || other._hasLayerViews; + bool mipViews = _hasMipViews || other._hasMipViews; + + if (layerViews != _hasLayerViews || mipViews != _hasMipViews) + { + _hasLayerViews = layerViews; + _hasMipViews = mipViews; + + RecalculateHandleRegions(); + } + + InheritHandles(other._handles, _handles); + } + + /// + /// Replace the current handles with the new handles. It is assumed that the new handles start dirty. + /// The dirty flags from the previous handles will be kept. + /// + /// The handles to replace the current handles with + private void ReplaceHandles(TextureGroupHandle[] handles) + { + if (_handles != null) + { + // When replacing handles, they should start as non-dirty. + + foreach (TextureGroupHandle groupHandle in handles) + { + foreach (CpuRegionHandle handle in groupHandle.Handles) + { + handle.Reprotect(); + } + } + + InheritHandles(_handles, handles); + + foreach (var oldGroup in _handles) + { + foreach (var oldHandle in oldGroup.Handles) + { + oldHandle.Dispose(); + } + } + } + + _handles = handles; + _loadNeeded = new bool[_handles.Length]; + } + + /// + /// Recalculate handle regions for this texture group, and inherit existing state into the new handles. + /// + private void RecalculateHandleRegions() + { + TextureGroupHandle[] handles; + + if (!(_hasMipViews || _hasLayerViews)) + { + // Single dirty region. + var cpuRegionHandles = new CpuRegionHandle[TextureRange.Count]; + + for (int i = 0; i < TextureRange.Count; i++) + { + var currentRange = TextureRange.GetSubRange(i); + cpuRegionHandles[i] = GenerateHandle(currentRange.Address, currentRange.Size); + } + + var groupHandle = new TextureGroupHandle(this, 0, Storage.Size, _views, 0, 0, cpuRegionHandles); + + foreach (CpuRegionHandle handle in cpuRegionHandles) + { + handle.RegisterDirtyEvent(() => DirtyAction(groupHandle)); + } + + handles = new TextureGroupHandle[] { groupHandle }; + } + else + { + // Get views for the host texture. + // It's worth noting that either the texture has layer views or mip views when getting to this point, which simplifies the logic a little. + // Depending on if the texture is 3d, either the mip views imply that layer views are present (2d) or the other way around (3d). + // This is enforced by the way the texture matched as a view, so we don't need to check. + + int layerHandles = _hasLayerViews ? _layers : 1; + int levelHandles = _hasMipViews ? _levels : 1; + + int handleIndex = 0; + + if (_is3D) + { + var handlesList = new List(); + + for (int i = 0; i < levelHandles; i++) + { + for (int j = 0; j < layerHandles; j++) + { + (int viewStart, int views) = Get3DLevelRange(i); + viewStart += j; + views = _hasLayerViews ? 1 : views; // A layer view is also a mip view. + + handlesList.Add(GenerateHandles(viewStart, views)); + } + + layerHandles = Math.Max(1, layerHandles >> 1); + } + + handles = handlesList.ToArray(); + } + else + { + handles = new TextureGroupHandle[layerHandles * levelHandles]; + + for (int i = 0; i < layerHandles; i++) + { + for (int j = 0; j < levelHandles; j++) + { + int viewStart = j + i * _levels; + int views = _hasMipViews ? 1 : _levels; // A mip view is also a layer view. + + handles[handleIndex++] = GenerateHandles(viewStart, views); + } + } + } + } + + ReplaceHandles(handles); + } + + /// + /// Ensure that there is a handle for each potential texture view. Required for copy dependencies to work. + /// + private void EnsureFullSubdivision() + { + if (!(_hasLayerViews && _hasMipViews)) + { + _hasLayerViews = true; + _hasMipViews = true; + + RecalculateHandleRegions(); + } + } + + /// + /// Create a copy dependency between this texture group, and a texture at a given layer/level offset. + /// + /// The view compatible texture to create a dependency to + /// The base layer of the given texture relative to the storage + /// The base level of the given texture relative to the storage + /// True if this texture is first copied to the given one, false for the opposite direction + public void CreateCopyDependency(Texture other, int firstLayer, int firstLevel, bool copyTo) + { + TextureGroup otherGroup = other.Group; + + EnsureFullSubdivision(); + otherGroup.EnsureFullSubdivision(); + + // Get the location of each texture within its storage, so we can find the handles to apply the dependency to. + // This can consist of multiple disjoint regions, for example if this is a mip slice of an array texture. + + var targetRange = new List<(int BaseHandle, int RegionCount)>(); + var otherRange = new List<(int BaseHandle, int RegionCount)>(); + + EvaluateRelevantHandles(firstLayer, firstLevel, other.Info.GetSlices(), other.Info.Levels, (baseHandle, regionCount, split) => targetRange.Add((baseHandle, regionCount))); + otherGroup.EvaluateRelevantHandles(other, (baseHandle, regionCount, split) => otherRange.Add((baseHandle, regionCount))); + + int targetIndex = 0; + int otherIndex = 0; + (int Handle, int RegionCount) targetRegion = (0, 0); + (int Handle, int RegionCount) otherRegion = (0, 0); + + while (true) + { + if (targetRegion.RegionCount == 0) + { + if (targetIndex >= targetRange.Count) + { + break; + } + + targetRegion = targetRange[targetIndex++]; + } + + if (otherRegion.RegionCount == 0) + { + if (otherIndex >= otherRange.Count) + { + break; + } + + otherRegion = otherRange[otherIndex++]; + } + + TextureGroupHandle handle = _handles[targetRegion.Handle++]; + TextureGroupHandle otherHandle = other.Group._handles[otherRegion.Handle++]; + + targetRegion.RegionCount--; + otherRegion.RegionCount--; + + handle.CreateCopyDependency(otherHandle, copyTo); + + // If "copyTo" is true, this texture must copy to the other. + // Otherwise, it must copy to this texture. + + if (copyTo) + { + otherHandle.Copy(handle); + } + else + { + handle.Copy(otherHandle); + } + } + } + + /// + /// A flush has been requested on a tracked region. Find an appropriate view to flush. + /// + /// The handle this flush action is for + /// The address of the flushing memory access + /// The size of the flushing memory access + public void FlushAction(TextureGroupHandle handle, ulong address, ulong size) + { + Storage.ExternalFlush(address, size); + + lock (handle.Overlaps) + { + foreach (Texture overlap in handle.Overlaps) + { + overlap.ExternalFlush(address, size); + } + } + + handle.Modified = false; + } + + /// + /// Dispose this texture group, disposing all related memory tracking handles. + /// + public void Dispose() + { + foreach (TextureGroupHandle group in _handles) + { + group.Dispose(); + } + } + } +} diff --git a/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs new file mode 100644 index 000000000..27ee1e499 --- /dev/null +++ b/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs @@ -0,0 +1,327 @@ +using Ryujinx.Cpu.Tracking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Graphics.Gpu.Image +{ + /// + /// A tracking handle for a texture group, which represents a range of views in a storage texture. + /// Retains a list of overlapping texture views, a modified flag, and tracking for each + /// CPU VA range that the views cover. + /// Also tracks copy dependencies for the handle - references to other handles that must be kept + /// in sync with this one before use. + /// + class TextureGroupHandle : IDisposable + { + private TextureGroup _group; + private int _bindCount; + private int _firstLevel; + private int _firstLayer; + + /// + /// The byte offset from the start of the storage of this handle. + /// + public int Offset { get; } + + /// + /// The size in bytes covered by this handle. + /// + public int Size { get; } + + /// + /// The textures which this handle overlaps with. + /// + public List Overlaps { get; } + + /// + /// The CPU memory tracking handles that cover this handle. + /// + public CpuRegionHandle[] Handles { get; } + + /// + /// True if a texture overlapping this handle has been modified. Is set false when the flush action is called. + /// + public bool Modified { get; set; } + + /// + /// Dependencies to handles from other texture groups. + /// + public List Dependencies { get; } + + /// + /// A flag indicating that a copy is required from one of the dependencies. + /// + public bool NeedsCopy => DeferredCopy != null; + + /// + /// A data copy that must be acknowledged the next time this handle is used. + /// + public TextureGroupHandle DeferredCopy { get; set; } + + /// + /// Create a new texture group handle, representing a range of views in a storage texture. + /// + /// The TextureGroup that the handle belongs to + /// The byte offset from the start of the storage of the handle + /// The size in bytes covered by the handle + /// All views of the storage texture, used to calculate overlaps + /// The first layer of this handle in the storage texture + /// The first level of this handle in the storage texture + /// The memory tracking handles that cover this handle + public TextureGroupHandle(TextureGroup group, int offset, ulong size, List views, int firstLayer, int firstLevel, CpuRegionHandle[] handles) + { + _group = group; + _firstLayer = firstLayer; + _firstLevel = firstLevel; + + Offset = offset; + Size = (int)size; + Overlaps = new List(); + Dependencies = new List(); + + if (views != null) + { + RecalculateOverlaps(group, views); + } + + Handles = handles; + } + + /// + /// Calculate a list of which views overlap this handle. + /// + /// The parent texture group, used to find a view's base CPU VA offset + /// The list of views to search for overlaps + public void RecalculateOverlaps(TextureGroup group, List views) + { + // Overlaps can be accessed from the memory tracking signal handler, so access must be atomic. + lock (Overlaps) + { + int endOffset = Offset + Size; + + Overlaps.Clear(); + + foreach (Texture view in views) + { + int viewOffset = group.FindOffset(view); + if (viewOffset < endOffset && Offset < viewOffset + (int)view.Size) + { + Overlaps.Add(view); + } + } + } + } + + /// + /// Signal that this handle has been modified to any existing dependencies, and set the modified flag. + /// + public void SignalModified() + { + Modified = true; + + // If this handle has any copy dependencies, notify the other handle that a copy needs to be performed. + + foreach (TextureDependency dependency in Dependencies) + { + dependency.SignalModified(); + } + } + + /// + /// Signal that this handle has either started or ended being modified. + /// + /// True if this handle is being bound, false if unbound + public void SignalModifying(bool bound) + { + SignalModified(); + + // Note: Bind count currently resets to 0 on inherit for safety, as the handle <-> view relationship can change. + _bindCount = Math.Max(0, _bindCount + (bound ? 1 : -1)); + } + + /// + /// Signal that a copy dependent texture has been modified, and must have its data copied to this one. + /// + /// The texture handle that must defer a copy to this one + public void DeferCopy(TextureGroupHandle copyFrom) + { + DeferredCopy = copyFrom; + + _group.Storage.SignalGroupDirty(); + + foreach (Texture overlap in Overlaps) + { + overlap.SignalGroupDirty(); + } + } + + /// + /// Create a copy dependency between this handle, and another. + /// + /// The handle to create a copy dependency to + /// True if a copy should be deferred to all of the other handle's dependencies + public void CreateCopyDependency(TextureGroupHandle other, bool copyToOther = false) + { + // Does this dependency already exist? + foreach (TextureDependency existing in Dependencies) + { + if (existing.Other.Handle == other) + { + // Do not need to create it again. May need to set the dirty flag. + return; + } + } + + _group.HasCopyDependencies = true; + other._group.HasCopyDependencies = true; + + TextureDependency dependency = new TextureDependency(this); + TextureDependency otherDependency = new TextureDependency(other); + + dependency.Other = otherDependency; + otherDependency.Other = dependency; + + Dependencies.Add(dependency); + other.Dependencies.Add(otherDependency); + + // Recursively create dependency: + // All of this handle's dependencies must depend on the other. + foreach (TextureDependency existing in Dependencies.ToArray()) + { + if (existing != dependency && existing.Other.Handle != other) + { + existing.Other.Handle.CreateCopyDependency(other); + } + } + + // All of the other handle's dependencies must depend on this. + foreach (TextureDependency existing in other.Dependencies.ToArray()) + { + if (existing != otherDependency && existing.Other.Handle != this) + { + existing.Other.Handle.CreateCopyDependency(this); + + if (copyToOther) + { + existing.Other.Handle.DeferCopy(this); + } + } + } + } + + /// + /// Remove a dependency from this handle's dependency list. + /// + /// The dependency to remove + public void RemoveDependency(TextureDependency dependency) + { + Dependencies.Remove(dependency); + } + + /// + /// Check if any of this handle's memory tracking handles are dirty. + /// + /// True if at least one of the handles is dirty + private bool CheckDirty() + { + return Handles.Any(handle => handle.Dirty); + } + + /// + /// Perform a copy from the provided handle to this one, or perform a deferred copy if none is provided. + /// + /// The handle to copy from. If not provided, this method will copy from and clear the deferred copy instead + /// True if the copy was performed, false otherwise + public bool Copy(TextureGroupHandle fromHandle = null) + { + bool result = false; + + if (fromHandle == null) + { + fromHandle = DeferredCopy; + + if (fromHandle != null && fromHandle._bindCount == 0) + { + // Repeat the copy in future if the bind count is greater than 0. + DeferredCopy = null; + } + } + + if (fromHandle != null) + { + // If the copy texture is dirty, do not copy. Its data no longer matters, and this handle should also be dirty. + if (!fromHandle.CheckDirty()) + { + Texture from = fromHandle._group.Storage; + Texture to = _group.Storage; + + if (from.ScaleFactor != to.ScaleFactor) + { + to.PropagateScale(from); + } + + from.HostTexture.CopyTo( + to.HostTexture, + fromHandle._firstLayer, + _firstLayer, + fromHandle._firstLevel, + _firstLevel); + + Modified = true; + + _group.RegisterAction(this); + + result = true; + } + } + + return result; + } + + /// + /// Inherit modified flags and dependencies from another texture handle. + /// + /// The texture handle to inherit from + public void Inherit(TextureGroupHandle old) + { + Modified |= old.Modified; + + foreach (TextureDependency dependency in old.Dependencies.ToArray()) + { + CreateCopyDependency(dependency.Other.Handle); + + if (dependency.Other.Handle.DeferredCopy == old) + { + dependency.Other.Handle.DeferredCopy = this; + } + } + + DeferredCopy = old.DeferredCopy; + } + + /// + /// Check if this region overlaps with another. + /// + /// Base address + /// Size of the region + /// True if overlapping, false otherwise + public bool OverlapsWith(int offset, int size) + { + return Offset < offset + size && offset < Offset + Size; + } + + public void Dispose() + { + foreach (CpuRegionHandle handle in Handles) + { + handle.Dispose(); + } + + foreach (TextureDependency dependency in Dependencies.ToArray()) + { + dependency.Other.Handle.RemoveDependency(dependency.Other); + } + } + } +} diff --git a/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs b/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs index 3137f8b8c..571f440e5 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureInfo.cs @@ -232,6 +232,31 @@ namespace Ryujinx.Graphics.Gpu.Image } } + /// + /// Gets the number of 2D slices of the texture. + /// Returns 6 for cubemap textures, layer faces for cubemap array textures, and DepthOrLayers for everything else. + /// + /// The number of texture slices + public int GetSlices() + { + if (Target == Target.Texture3D || Target == Target.Texture2DArray || Target == Target.Texture2DMultisampleArray) + { + return DepthOrLayers; + } + else if (Target == Target.CubemapArray) + { + return DepthOrLayers * 6; + } + else if (Target == Target.Cubemap) + { + return 6; + } + else + { + return 1; + } + } + /// /// Calculates the size information from the texture information. /// diff --git a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs index 2646a75b7..f13c3443d 100644 --- a/Ryujinx.Graphics.Gpu/Image/TextureManager.cs +++ b/Ryujinx.Graphics.Gpu/Image/TextureManager.cs @@ -185,7 +185,14 @@ namespace Ryujinx.Graphics.Gpu.Image { bool hasValue = color != null; bool changesScale = (hasValue != (_rtColors[index] != null)) || (hasValue && RenderTargetScale != color.ScaleFactor); - _rtColors[index] = color; + + if (_rtColors[index] != color) + { + _rtColors[index]?.SignalModifying(false); + color?.SignalModifying(true); + + _rtColors[index] = color; + } return changesScale || (hasValue && color.ScaleMode != TextureScaleMode.Blacklisted && color.ScaleFactor != GraphicsConfig.ResScale); } @@ -292,7 +299,14 @@ namespace Ryujinx.Graphics.Gpu.Image { bool hasValue = depthStencil != null; bool changesScale = (hasValue != (_rtDepthStencil != null)) || (hasValue && RenderTargetScale != depthStencil.ScaleFactor); - _rtDepthStencil = depthStencil; + + if (_rtDepthStencil != depthStencil) + { + _rtDepthStencil?.SignalModifying(false); + depthStencil?.SignalModifying(true); + + _rtDepthStencil = depthStencil; + } return changesScale || (hasValue && depthStencil.ScaleMode != TextureScaleMode.Blacklisted && depthStencil.ScaleFactor != GraphicsConfig.ResScale); } @@ -754,38 +768,97 @@ namespace Ryujinx.Graphics.Gpu.Image overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps); } + if (_overlapInfo.Length != _textureOverlaps.Length) + { + Array.Resize(ref _overlapInfo, _textureOverlaps.Length); + } + + // =============== Find Texture View of Existing Texture =============== + + int fullyCompatible = 0; + + // Evaluate compatibility of overlaps + for (int index = 0; index < overlapsCount; index++) { Texture overlap = _textureOverlaps[index]; - TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, out int firstLayer, out int firstLevel); + TextureViewCompatibility overlapCompatibility = overlap.IsViewCompatible(info, range.Value, sizeInfo.LayerSize, out int firstLayer, out int firstLevel); if (overlapCompatibility == TextureViewCompatibility.Full) { - TextureInfo oInfo = AdjustSizes(overlap, info, firstLevel); + if (overlap.IsView) + { + overlapCompatibility = TextureViewCompatibility.CopyOnly; + } + else + { + fullyCompatible++; + } + } + + _overlapInfo[index] = new OverlapInfo(overlapCompatibility, firstLayer, firstLevel); + } + + // Search through the overlaps to find a compatible view and establish any copy dependencies. + + for (int index = 0; index < overlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; + + if (oInfo.Compatibility == TextureViewCompatibility.Full) + { + TextureInfo adjInfo = AdjustSizes(overlap, info, oInfo.FirstLevel); if (!isSamplerTexture) { - info = oInfo; + info = adjInfo; } - texture = overlap.CreateView(oInfo, sizeInfo, range.Value, firstLayer, firstLevel); - - if (overlap.IsModified) - { - texture.SignalModified(); - } + texture = overlap.CreateView(adjInfo, sizeInfo, range.Value, oInfo.FirstLayer, oInfo.FirstLevel); ChangeSizeIfNeeded(info, texture, isSamplerTexture, sizeHint); + texture.SynchronizeMemory(); break; } - else if (overlapCompatibility == TextureViewCompatibility.CopyOnly) + else if (oInfo.Compatibility == TextureViewCompatibility.CopyOnly && fullyCompatible == 0) { - // TODO: Copy rules for targets created after the container texture. See below. - overlap.DisableMemoryTracking(); + // Only copy compatible. If there's another choice for a FULLY compatible texture, choose that instead. + + texture = new Texture(_context, info, sizeInfo, range.Value, scaleMode); + texture.InitializeGroup(true, true); + texture.InitializeData(false, false); + + overlap.SynchronizeMemory(); + overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true); + break; } } + if (texture != null) + { + // This texture could be a view of multiple parent textures with different storages, even if it is a view. + // When a texture is created, make sure all possible dependencies to other textures are created as copies. + // (even if it could be fulfilled without a copy) + + for (int index = 0; index < overlapsCount; index++) + { + Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; + + if (oInfo.Compatibility != TextureViewCompatibility.Incompatible && overlap.Group != texture.Group) + { + overlap.SynchronizeMemory(); + overlap.CreateCopyDependency(texture, oInfo.FirstLayer, oInfo.FirstLevel, true); + } + } + + texture.SynchronizeMemory(); + } + + // =============== Create a New Texture =============== + // No match, create a new texture. if (texture == null) { @@ -795,24 +868,53 @@ namespace Ryujinx.Graphics.Gpu.Image // Any textures that are incompatible will contain garbage data, so they should be removed where possible. int viewCompatible = 0; + fullyCompatible = 0; bool setData = isSamplerTexture || overlapsCount == 0 || flags.HasFlag(TextureSearchFlags.ForCopy); + bool hasLayerViews = false; + bool hasMipViews = false; + for (int index = 0; index < overlapsCount; index++) { Texture overlap = _textureOverlaps[index]; bool overlapInCache = overlap.CacheNode != null; - TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, out int firstLayer, out int firstLevel); + TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Range, overlap.LayerSize, out int firstLayer, out int firstLevel); + + if (overlap.IsView && compatibility == TextureViewCompatibility.Full) + { + compatibility = TextureViewCompatibility.CopyOnly; + } if (compatibility != TextureViewCompatibility.Incompatible) { - if (_overlapInfo.Length != _textureOverlaps.Length) + if (compatibility == TextureViewCompatibility.Full) { - Array.Resize(ref _overlapInfo, _textureOverlaps.Length); + if (viewCompatible == fullyCompatible) + { + _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); + _textureOverlaps[viewCompatible++] = overlap; + } + else + { + // Swap overlaps so that the fully compatible views have priority. + + _overlapInfo[viewCompatible] = _overlapInfo[fullyCompatible]; + _textureOverlaps[viewCompatible++] = _textureOverlaps[fullyCompatible]; + + _overlapInfo[fullyCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); + _textureOverlaps[fullyCompatible] = overlap; + } + fullyCompatible++; + } + else + { + _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); + _textureOverlaps[viewCompatible++] = overlap; } - _overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel); - _textureOverlaps[viewCompatible++] = overlap; + hasLayerViews |= overlap.Info.GetSlices() < texture.Info.GetSlices(); + hasMipViews |= overlap.Info.Levels < texture.Info.Levels; } else if (overlapInCache || !setData) { @@ -841,6 +943,8 @@ namespace Ryujinx.Graphics.Gpu.Image } } + texture.InitializeGroup(hasLayerViews, hasMipViews); + // 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); @@ -848,17 +952,17 @@ namespace Ryujinx.Graphics.Gpu.Image for (int index = 0; index < viewCompatible; index++) { Texture overlap = _textureOverlaps[index]; + OverlapInfo oInfo = _overlapInfo[index]; - if (oInfo.Compatibility != TextureViewCompatibility.Full) + if (overlap.Group == texture.Group) { - continue; // Copy only compatibilty. + // If the texture group is equal, then this texture (or its parent) is already a view. + continue; } TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, oInfo.FirstLevel); - TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor); - 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. @@ -867,47 +971,30 @@ namespace Ryujinx.Graphics.Gpu.Image 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 (overlap.IsModified) + if (oInfo.Compatibility != TextureViewCompatibility.Full) { - texture.SignalModified(); + // Copy only compatibility, or target texture is already a view. + + ChangeSizeIfNeeded(overlapInfo, overlap, false, sizeHint); // Force a size match for copy + + overlap.SynchronizeMemory(); + texture.CreateCopyDependency(overlap, oInfo.FirstLayer, oInfo.FirstLevel, false); } - - 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 && viewCompatible > 0) - { - // TODO: This copy can currently only happen when the 3D texture is created. - // If a game clears and redraws the slices, we won't be able to copy the new data to the 3D texture. - // Disable tracking to try keep at least the original data in there for as long as possible. - texture.DisableMemoryTracking(); - - for (int index = 0; index < viewCompatible; index++) + else { - Texture overlap = _textureOverlaps[index]; - OverlapInfo oInfo = _overlapInfo[index]; + TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities, overlap.ScaleFactor); - if (oInfo.Compatibility != TextureViewCompatibility.Incompatible) - { - overlap.BlacklistScale(); + ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel); - overlap.HostTexture.CopyTo(texture.HostTexture, oInfo.FirstLayer, oInfo.FirstLevel); + overlap.SynchronizeMemory(); - if (overlap.IsModified) - { - texture.SignalModified(); - } - } + overlap.HostTexture.CopyTo(newView, 0, 0); + + overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel); } } + + texture.SynchronizeMemory(); } // Sampler textures are managed by the texture pool, all other textures diff --git a/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs b/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs index d2a054953..92099b6a5 100644 --- a/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs +++ b/Ryujinx.Graphics.Gpu/Memory/GpuRegionHandle.cs @@ -49,11 +49,11 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - public void Reprotect() + public void Reprotect(bool asDirty = false) { foreach (var regionHandle in _cpuRegionHandles) { - regionHandle.Reprotect(); + regionHandle.Reprotect(asDirty); } } } diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs index 6df2b630c..5607fb401 100644 --- a/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs +++ b/Ryujinx.Graphics.OpenGL/Image/TextureBuffer.cs @@ -18,6 +18,11 @@ namespace Ryujinx.Graphics.OpenGL.Image throw new NotSupportedException(); } + public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel) + { + throw new NotSupportedException(); + } + public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter) { throw new NotSupportedException(); @@ -38,6 +43,11 @@ namespace Ryujinx.Graphics.OpenGL.Image Buffer.SetData(_buffer, _bufferOffset, data.Slice(0, Math.Min(data.Length, _bufferSize))); } + public void SetData(ReadOnlySpan data, int layer, int level) + { + throw new NotSupportedException(); + } + public void SetStorage(BufferRange buffer) { if (buffer.Handle == _buffer && diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs b/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs index 20a3b9149..b27403b22 100644 --- a/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs +++ b/Ryujinx.Graphics.OpenGL/Image/TextureCopy.cs @@ -115,18 +115,44 @@ namespace Ryujinx.Graphics.OpenGL.Image TextureCreateInfo srcInfo = src.Info; TextureCreateInfo dstInfo = dst.Info; + int srcDepth = srcInfo.GetDepthOrLayers(); + int srcLevels = srcInfo.Levels; + + int dstDepth = dstInfo.GetDepthOrLayers(); + int dstLevels = dstInfo.Levels; + + if (dstInfo.Target == Target.Texture3D) + { + dstDepth = Math.Max(1, dstDepth >> dstLevel); + } + + int depth = Math.Min(srcDepth, dstDepth); + int levels = Math.Min(srcLevels, dstLevels); + + CopyUnscaled(src, dst, srcLayer, dstLayer, srcLevel, dstLevel, depth, levels); + } + + public void CopyUnscaled( + ITextureInfo src, + ITextureInfo dst, + int srcLayer, + int dstLayer, + int srcLevel, + int dstLevel, + int depth, + int levels) + { + TextureCreateInfo srcInfo = src.Info; + TextureCreateInfo dstInfo = dst.Info; + int srcHandle = src.Handle; int dstHandle = dst.Handle; int srcWidth = srcInfo.Width; int srcHeight = srcInfo.Height; - int srcDepth = srcInfo.GetDepthOrLayers(); - int srcLevels = srcInfo.Levels; int dstWidth = dstInfo.Width; int dstHeight = dstInfo.Height; - int dstDepth = dstInfo.GetDepthOrLayers(); - int dstLevels = dstInfo.Levels; srcWidth = Math.Max(1, srcWidth >> srcLevel); srcHeight = Math.Max(1, srcHeight >> srcLevel); @@ -134,11 +160,6 @@ namespace Ryujinx.Graphics.OpenGL.Image dstWidth = Math.Max(1, dstWidth >> dstLevel); dstHeight = Math.Max(1, dstHeight >> dstLevel); - if (dstInfo.Target == Target.Texture3D) - { - dstDepth = Math.Max(1, dstDepth >> dstLevel); - } - int blockWidth = 1; int blockHeight = 1; bool sizeInBlocks = false; @@ -166,8 +187,6 @@ namespace Ryujinx.Graphics.OpenGL.Image int width = Math.Min(srcWidth, dstWidth); int height = Math.Min(srcHeight, dstHeight); - int depth = Math.Min(srcDepth, dstDepth); - int levels = Math.Min(srcLevels, dstLevels); for (int level = 0; level < levels; level++) { diff --git a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs index 89f74ceec..053fb3c24 100644 --- a/Ryujinx.Graphics.OpenGL/Image/TextureView.cs +++ b/Ryujinx.Graphics.OpenGL/Image/TextureView.cs @@ -10,8 +10,6 @@ namespace Ryujinx.Graphics.OpenGL.Image private readonly TextureStorage _parent; - private TextureView _emulatedViewParent; - private TextureView _incompatibleFormatView; public int FirstLayer { get; private set; } @@ -96,37 +94,10 @@ namespace Ryujinx.Graphics.OpenGL.Image public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) { - if (Info.IsCompressed == info.IsCompressed) - { - firstLayer += FirstLayer; - firstLevel += FirstLevel; + firstLayer += FirstLayer; + firstLevel += FirstLevel; - return _parent.CreateView(info, firstLayer, firstLevel); - } - else - { - // TODO: Most graphics APIs doesn't support creating a texture view from a compressed format - // with a non-compressed format (or vice-versa), however NVN seems to support it. - // So we emulate that here with a texture copy (see the first CopyTo overload). - // However right now it only does a single copy right after the view is created, - // so it doesn't work for all cases. - TextureView emulatedView = (TextureView)_renderer.CreateTexture(info, ScaleFactor); - - _renderer.TextureCopy.CopyUnscaled( - this, - emulatedView, - 0, - firstLayer, - 0, - firstLevel); - - emulatedView._emulatedViewParent = this; - - emulatedView.FirstLayer = firstLayer; - emulatedView.FirstLevel = firstLevel; - - return emulatedView; - } + return _parent.CreateView(info, firstLayer, firstLevel); } public int GetIncompatibleFormatViewHandle() @@ -163,17 +134,13 @@ namespace Ryujinx.Graphics.OpenGL.Image TextureView destinationView = (TextureView)destination; _renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel); + } - if (destinationView._emulatedViewParent != null) - { - _renderer.TextureCopy.CopyUnscaled( - this, - destinationView._emulatedViewParent, - 0, - destinationView.FirstLayer, - 0, - destinationView.FirstLevel); - } + public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel) + { + TextureView destinationView = (TextureView)destination; + + _renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); } public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter) @@ -308,6 +275,20 @@ namespace Ryujinx.Graphics.OpenGL.Image } } + public void SetData(ReadOnlySpan data, int layer, int level) + { + unsafe + { + fixed (byte* ptr = data) + { + int width = Math.Max(Info.Width >> level, 1); + int height = Math.Max(Info.Height >> level, 1); + + ReadFrom2D((IntPtr)ptr, layer, level, width, height); + } + } + } + public void ReadFromPbo(int offset, int size) { ReadFrom(IntPtr.Zero + offset, size); diff --git a/Ryujinx.Graphics.Texture/SizeCalculator.cs b/Ryujinx.Graphics.Texture/SizeCalculator.cs index 2dc608697..7eee13c0c 100644 --- a/Ryujinx.Graphics.Texture/SizeCalculator.cs +++ b/Ryujinx.Graphics.Texture/SizeCalculator.cs @@ -9,6 +9,19 @@ namespace Ryujinx.Graphics.Texture { private const int StrideAlignment = 32; + private static int Calculate3DOffsetCount(int levels, int depth) + { + int offsetCount = depth; + + while (--levels > 0) + { + depth = Math.Max(1, depth >> 1); + offsetCount += depth; + } + + return offsetCount; + } + public static SizeInfo GetBlockLinearTextureSize( int width, int height, @@ -27,8 +40,9 @@ namespace Ryujinx.Graphics.Texture int layerSize = 0; - int[] allOffsets = new int[levels * layers * depth]; + int[] allOffsets = new int[is3D ? Calculate3DOffsetCount(levels, depth) : levels * layers * depth]; int[] mipOffsets = new int[levels]; + int[] sliceSizes = new int[levels]; int mipGobBlocksInY = gobBlocksInY; int mipGobBlocksInZ = gobBlocksInZ; @@ -36,6 +50,8 @@ namespace Ryujinx.Graphics.Texture int gobWidth = (GobStride / bytesPerPixel) * gobBlocksInTileX; int gobHeight = gobBlocksInY * GobHeight; + int depthLevelOffset = 0; + for (int level = 0; level < levels; level++) { int w = Math.Max(1, width >> level); @@ -86,13 +102,16 @@ namespace Ryujinx.Graphics.Texture int zLow = z & mask; int zHigh = z & ~mask; - allOffsets[z * levels + level] = baseOffset + zLow * gobSize + zHigh * sliceSize; + allOffsets[z + depthLevelOffset] = baseOffset + zLow * gobSize + zHigh * sliceSize; } } mipOffsets[level] = layerSize; + sliceSizes[level] = totalBlocksOfGobsInY * robSize; - layerSize += totalBlocksOfGobsInZ * totalBlocksOfGobsInY * robSize; + layerSize += totalBlocksOfGobsInZ * sliceSizes[level]; + + depthLevelOffset += d; } if (layers > 1) @@ -133,7 +152,7 @@ namespace Ryujinx.Graphics.Texture } } - return new SizeInfo(mipOffsets, allOffsets, levels, layerSize, totalSize); + return new SizeInfo(mipOffsets, allOffsets, sliceSizes, depth, levels, layerSize, totalSize, is3D); } public static SizeInfo GetLinearTextureSize(int stride, int height, int blockHeight) @@ -142,7 +161,7 @@ namespace Ryujinx.Graphics.Texture // so we only need to handle a single case (2D textures without mipmaps). int totalSize = stride * BitUtils.DivRoundUp(height, blockHeight); - return new SizeInfo(new int[] { 0 }, new int[] { 0 }, 1, totalSize, totalSize); + return new SizeInfo(totalSize); } private static int AlignLayerSize( diff --git a/Ryujinx.Graphics.Texture/SizeInfo.cs b/Ryujinx.Graphics.Texture/SizeInfo.cs index 55b22e3af..f518ee4b1 100644 --- a/Ryujinx.Graphics.Texture/SizeInfo.cs +++ b/Ryujinx.Graphics.Texture/SizeInfo.cs @@ -5,34 +5,46 @@ namespace Ryujinx.Graphics.Texture public struct SizeInfo { private readonly int[] _mipOffsets; - private readonly int[] _allOffsets; private readonly int _levels; + private readonly int _depth; + private readonly bool _is3D; + public readonly int[] AllOffsets; + public readonly int[] SliceSizes; public int LayerSize { get; } public int TotalSize { get; } public SizeInfo(int size) { _mipOffsets = new int[] { 0 }; - _allOffsets = new int[] { 0 }; + AllOffsets = new int[] { 0 }; + SliceSizes = new int[] { size }; + _depth = 1; _levels = 1; LayerSize = size; TotalSize = size; + _is3D = false; } internal SizeInfo( int[] mipOffsets, int[] allOffsets, + int[] sliceSizes, + int depth, int levels, int layerSize, - int totalSize) + int totalSize, + bool is3D) { _mipOffsets = mipOffsets; - _allOffsets = allOffsets; + AllOffsets = allOffsets; + SliceSizes = sliceSizes; + _depth = depth; _levels = levels; LayerSize = layerSize; TotalSize = totalSize; + _is3D = is3D; } public int GetMipOffset(int level) @@ -47,7 +59,7 @@ namespace Ryujinx.Graphics.Texture public bool FindView(int offset, out int firstLayer, out int firstLevel) { - int index = Array.BinarySearch(_allOffsets, offset); + int index = Array.BinarySearch(AllOffsets, offset); if (index < 0) { @@ -57,8 +69,25 @@ namespace Ryujinx.Graphics.Texture return false; } - firstLayer = index / _levels; - firstLevel = index - (firstLayer * _levels); + if (_is3D) + { + firstLayer = index; + firstLevel = 0; + + int levelDepth = _depth; + + while (firstLayer >= levelDepth) + { + firstLayer -= levelDepth; + firstLevel++; + levelDepth = Math.Max(levelDepth >> 1, 1); + } + } + else + { + firstLayer = index / _levels; + firstLevel = index - (firstLayer * _levels); + } return true; } diff --git a/Ryujinx.Memory/Tracking/IRegionHandle.cs b/Ryujinx.Memory/Tracking/IRegionHandle.cs index 33628da64..cd33e5c8f 100644 --- a/Ryujinx.Memory/Tracking/IRegionHandle.cs +++ b/Ryujinx.Memory/Tracking/IRegionHandle.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Memory.Tracking ulong Size { get; } ulong EndAddress { get; } - void Reprotect(); + void Reprotect(bool asDirty = false); void RegisterAction(RegionSignal action); } } diff --git a/Ryujinx.Memory/Tracking/RegionHandle.cs b/Ryujinx.Memory/Tracking/RegionHandle.cs index 3ddcb6db4..4da184dd7 100644 --- a/Ryujinx.Memory/Tracking/RegionHandle.cs +++ b/Ryujinx.Memory/Tracking/RegionHandle.cs @@ -1,4 +1,5 @@ using Ryujinx.Memory.Range; +using System; using System.Collections.Generic; using System.Threading; @@ -19,9 +20,12 @@ namespace Ryujinx.Memory.Tracking internal IMultiRegionHandle Parent { get; set; } internal int SequenceNumber { get; set; } + private event Action _onDirty; + private RegionSignal _preAction; // Action to perform before a read or write. This will block the memory access. private readonly List _regions; private readonly MemoryTracking _tracking; + private bool _disposed; internal MemoryPermission RequiredPermission => _preAction != null ? MemoryPermission.None : (Dirty ? MemoryPermission.ReadAndWrite : MemoryPermission.Read); internal RegionSignal PreAction => _preAction; @@ -60,7 +64,12 @@ namespace Ryujinx.Memory.Tracking if (write) { + bool oldDirty = Dirty; Dirty = true; + if (!oldDirty) + { + _onDirty?.Invoke(); + } Parent?.SignalWrite(); } } @@ -68,9 +77,9 @@ namespace Ryujinx.Memory.Tracking /// /// Consume the dirty flag for this handle, and reprotect so it can be set on the next write. /// - public void Reprotect() + public void Reprotect(bool asDirty = false) { - Dirty = false; + Dirty = asDirty; lock (_tracking.TrackingLock) { foreach (VirtualRegion region in _regions) @@ -100,6 +109,16 @@ namespace Ryujinx.Memory.Tracking } } + /// + /// Register an action to perform when the region is written to. + /// This action will not be removed when it is called - it is called each time the dirty flag is set. + /// + /// Action to call on dirty + public void RegisterDirtyEvent(Action action) + { + _onDirty += action; + } + /// /// Add a child virtual region to this handle. /// @@ -125,6 +144,13 @@ namespace Ryujinx.Memory.Tracking /// public void Dispose() { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + + _disposed = true; + lock (_tracking.TrackingLock) { foreach (VirtualRegion region in _regions)