Ryujinx/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
riperiperi da073fce61
GPU: Fast path for adding one texture view to a group (#4528)
* GPU: Fast path for adding one texture view to a group

Texture group handles must store a list of their overlapping views, so they can be properly notified when a write is detected, and a few other things relating to texture readback. This is generally created when the group is established, with each handle looping over all views to find its overlaps. This whole process was also done when only a single view was added (and no handles were changed), however...

Sonic Frontiers had a huge cubemap array with 7350 faces (175 cubemaps * 6 faces * 7 levels), so iterating over both handles and existing views added up very fast. Since we are only adding a single view, we only need to _add_ that view to the existing overlaps, rather than recalculate them all.

This greatly improves performance during loading screens and a few seconds into gameplay on the "open zone" sections of Sonic Frontiers. May improve loading times or stutters on some other games.

Note that the current texture cache rules will cause these views to fall out of the cache, as there are more than the hard cap, so the cost will be repaid when reloading the open zone.

I also added some code to properly remove overlaps when texture views are removed, since it seems that was missing.

This can be improved further by only iterating handles that overlap the view (filter by range), but so can a few places in TextureGroup, so better to do all at once. The full generation of overlaps could probably be improved in a similar way.

I recommend testing a few games to make sure nothing breaks.

* Address feedback
2023-03-14 17:33:44 -03:00

555 lines
20 KiB
C#

using Ryujinx.Cpu.Tracking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// A tracking handle for a texture group, which represents a range of views in a storage texture.
/// Retains a list of overlapping texture views, a modified flag, and tracking for each
/// CPU VA range that the views cover.
/// Also tracks copy dependencies for the handle - references to other handles that must be kept
/// in sync with this one before use.
/// </summary>
class TextureGroupHandle : IDisposable
{
private TextureGroup _group;
private int _bindCount;
private int _firstLevel;
private int _firstLayer;
// Sync state for texture flush.
/// <summary>
/// The sync number last registered.
/// </summary>
private ulong _registeredSync;
/// <summary>
/// The sync number when the texture was last modified by GPU.
/// </summary>
private ulong _modifiedSync;
/// <summary>
/// Whether a tracking action is currently registered or not. (0/1)
/// </summary>
private int _actionRegistered;
/// <summary>
/// Whether a sync action is currently registered or not.
/// </summary>
private bool _syncActionRegistered;
/// <summary>
/// The byte offset from the start of the storage of this handle.
/// </summary>
public int Offset { get; }
/// <summary>
/// The size in bytes covered by this handle.
/// </summary>
public int Size { get; }
/// <summary>
/// The base slice index for this handle.
/// </summary>
public int BaseSlice { get; }
/// <summary>
/// The number of slices covered by this handle.
/// </summary>
public int SliceCount { get; }
/// <summary>
/// The textures which this handle overlaps with.
/// </summary>
public List<Texture> Overlaps { get; }
/// <summary>
/// The CPU memory tracking handles that cover this handle.
/// </summary>
public CpuRegionHandle[] Handles { get; }
/// <summary>
/// True if a texture overlapping this handle has been modified. Is set false when the flush action is called.
/// </summary>
public bool Modified { get; set; }
/// <summary>
/// Dependencies to handles from other texture groups.
/// </summary>
public List<TextureDependency> Dependencies { get; }
/// <summary>
/// A flag indicating that a copy is required from one of the dependencies.
/// </summary>
public bool NeedsCopy => DeferredCopy != null;
/// <summary>
/// A data copy that must be acknowledged the next time this handle is used.
/// </summary>
public TextureGroupHandle DeferredCopy { get; set; }
/// <summary>
/// Create a new texture group handle, representing a range of views in a storage texture.
/// </summary>
/// <param name="group">The TextureGroup that the handle belongs to</param>
/// <param name="offset">The byte offset from the start of the storage of the handle</param>
/// <param name="size">The size in bytes covered by the handle</param>
/// <param name="views">All views of the storage texture, used to calculate overlaps</param>
/// <param name="firstLayer">The first layer of this handle in the storage texture</param>
/// <param name="firstLevel">The first level of this handle in the storage texture</param>
/// <param name="baseSlice">The base slice index of this handle</param>
/// <param name="sliceCount">The number of slices this handle covers</param>
/// <param name="handles">The memory tracking handles that cover this handle</param>
public TextureGroupHandle(TextureGroup group,
int offset,
ulong size,
List<Texture> views,
int firstLayer,
int firstLevel,
int baseSlice,
int sliceCount,
CpuRegionHandle[] handles)
{
_group = group;
_firstLayer = firstLayer;
_firstLevel = firstLevel;
Offset = offset;
Size = (int)size;
Overlaps = new List<Texture>();
Dependencies = new List<TextureDependency>();
BaseSlice = baseSlice;
SliceCount = sliceCount;
if (views != null)
{
RecalculateOverlaps(group, views);
}
Handles = handles;
}
/// <summary>
/// Calculate a list of which views overlap this handle.
/// </summary>
/// <param name="group">The parent texture group, used to find a view's base CPU VA offset</param>
/// <param name="views">The list of views to search for overlaps</param>
public void RecalculateOverlaps(TextureGroup group, List<Texture> views)
{
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
lock (Overlaps)
{
int endOffset = Offset + Size;
Overlaps.Clear();
foreach (Texture view in views)
{
int viewOffset = group.FindOffset(view);
if (viewOffset < endOffset && Offset < viewOffset + (int)view.Size)
{
Overlaps.Add(view);
}
}
}
}
/// <summary>
/// Adds a single texture view as an overlap if its range overlaps.
/// </summary>
/// <param name="offset">The offset of the view in the group</param>
/// <param name="view">The texture to add as an overlap</param>
public void AddOverlap(int offset, Texture view)
{
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
if (OverlapsWith(offset, (int)view.Size))
{
lock (Overlaps)
{
Overlaps.Add(view);
}
}
}
/// <summary>
/// Removes a single texture view as an overlap if its range overlaps.
/// </summary>
/// <param name="offset">The offset of the view in the group</param>
/// <param name="view">The texture to add as an overlap</param>
public void RemoveOverlap(int offset, Texture view)
{
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
if (OverlapsWith(offset, (int)view.Size))
{
lock (Overlaps)
{
Overlaps.Remove(view);
}
}
}
/// <summary>
/// Registers a sync action to happen for this handle, and an interim flush action on the tracking handle.
/// </summary>
/// <param name="context">The GPU context to register a sync action on</param>
private void RegisterSync(GpuContext context)
{
if (!_syncActionRegistered)
{
_modifiedSync = context.SyncNumber;
context.RegisterSyncAction(SyncAction, true);
_syncActionRegistered = true;
}
if (Interlocked.Exchange(ref _actionRegistered, 1) == 0)
{
_group.RegisterAction(this);
}
}
/// <summary>
/// Signal that this handle has been modified to any existing dependencies, and set the modified flag.
/// </summary>
/// <param name="context">The GPU context to register a sync action on</param>
public void SignalModified(GpuContext context)
{
Modified = true;
// If this handle has any copy dependencies, notify the other handle that a copy needs to be performed.
foreach (TextureDependency dependency in Dependencies)
{
dependency.SignalModified();
}
RegisterSync(context);
}
/// <summary>
/// Signal that this handle has either started or ended being modified.
/// </summary>
/// <param name="bound">True if this handle is being bound, false if unbound</param>
/// <param name="context">The GPU context to register a sync action on</param>
public void SignalModifying(bool bound, GpuContext context)
{
SignalModified(context);
// Note: Bind count currently resets to 0 on inherit for safety, as the handle <-> view relationship can change.
_bindCount = Math.Max(0, _bindCount + (bound ? 1 : -1));
}
/// <summary>
/// Synchronize dependent textures, if any of them have deferred a copy from this texture.
/// </summary>
public void SynchronizeDependents()
{
foreach (TextureDependency dependency in Dependencies)
{
TextureGroupHandle otherHandle = dependency.Other.Handle;
if (otherHandle.DeferredCopy == this)
{
otherHandle._group.Storage.SynchronizeMemory();
}
}
}
/// <summary>
/// Wait for the latest sync number that the texture handle was written to,
/// removing the modified flag if it was reached, or leaving it set if it has not yet been created.
/// </summary>
/// <param name="context">The GPU context used to wait for sync</param>
public void Sync(GpuContext context)
{
ulong registeredSync = _registeredSync;
long diff = (long)(context.SyncNumber - registeredSync);
if (diff > 0)
{
context.Renderer.WaitSync(registeredSync);
if ((long)(_modifiedSync - registeredSync) > 0)
{
// Flush the data in a previous state. Do not remove the modified flag - it will be removed to ignore following writes.
return;
}
Modified = false;
}
// If the difference is <= 0, no data is not ready yet. Flush any data we can without waiting or removing modified flag.
}
/// <summary>
/// Clears the action registered variable, indicating that the tracking action should be
/// re-registered on the next modification.
/// </summary>
public void ClearActionRegistered()
{
Interlocked.Exchange(ref _actionRegistered, 0);
}
/// <summary>
/// Action to perform when a sync number is registered after modification.
/// This action will register a read tracking action on the memory tracking handle so that a flush from CPU can happen.
/// </summary>
private void SyncAction()
{
// The storage will need to signal modified again to update the sync number in future.
_group.Storage.SignalModifiedDirty();
lock (Overlaps)
{
foreach (Texture texture in Overlaps)
{
texture.SignalModifiedDirty();
}
}
// Register region tracking for CPU? (again)
_registeredSync = _modifiedSync;
_syncActionRegistered = false;
if (Interlocked.Exchange(ref _actionRegistered, 1) == 0)
{
_group.RegisterAction(this);
}
}
/// <summary>
/// Signal that a copy dependent texture has been modified, and must have its data copied to this one.
/// </summary>
/// <param name="copyFrom">The texture handle that must defer a copy to this one</param>
public void DeferCopy(TextureGroupHandle copyFrom)
{
Modified = false;
DeferredCopy = copyFrom;
_group.Storage.SignalGroupDirty();
foreach (Texture overlap in Overlaps)
{
overlap.SignalGroupDirty();
}
}
/// <summary>
/// Create a copy dependency between this handle, and another.
/// </summary>
/// <param name="other">The handle to create a copy dependency to</param>
/// <param name="copyToOther">True if a copy should be deferred to all of the other handle's dependencies</param>
public void CreateCopyDependency(TextureGroupHandle other, bool copyToOther = false)
{
// Does this dependency already exist?
foreach (TextureDependency existing in Dependencies)
{
if (existing.Other.Handle == other)
{
// Do not need to create it again. May need to set the dirty flag.
return;
}
}
_group.HasCopyDependencies = true;
other._group.HasCopyDependencies = true;
TextureDependency dependency = new TextureDependency(this);
TextureDependency otherDependency = new TextureDependency(other);
dependency.Other = otherDependency;
otherDependency.Other = dependency;
Dependencies.Add(dependency);
other.Dependencies.Add(otherDependency);
// Recursively create dependency:
// All of this handle's dependencies must depend on the other.
foreach (TextureDependency existing in Dependencies.ToArray())
{
if (existing != dependency && existing.Other.Handle != other)
{
existing.Other.Handle.CreateCopyDependency(other);
}
}
// All of the other handle's dependencies must depend on this.
foreach (TextureDependency existing in other.Dependencies.ToArray())
{
if (existing != otherDependency && existing.Other.Handle != this)
{
existing.Other.Handle.CreateCopyDependency(this);
if (copyToOther)
{
existing.Other.Handle.DeferCopy(this);
}
}
}
}
/// <summary>
/// Remove a dependency from this handle's dependency list.
/// </summary>
/// <param name="dependency">The dependency to remove</param>
public void RemoveDependency(TextureDependency dependency)
{
Dependencies.Remove(dependency);
}
/// <summary>
/// Check if any of this handle's memory tracking handles are dirty.
/// </summary>
/// <returns>True if at least one of the handles is dirty</returns>
private bool CheckDirty()
{
return Handles.Any(handle => handle.Dirty);
}
/// <summary>
/// Perform a copy from the provided handle to this one, or perform a deferred copy if none is provided.
/// </summary>
/// <param name="context">GPU context to register sync for modified handles</param>
/// <param name="fromHandle">The handle to copy from. If not provided, this method will copy from and clear the deferred copy instead</param>
/// <returns>True if the copy was performed, false otherwise</returns>
public bool Copy(GpuContext context, TextureGroupHandle fromHandle = null)
{
bool result = false;
bool shouldCopy = false;
if (fromHandle == null)
{
fromHandle = DeferredCopy;
if (fromHandle != null)
{
// Only copy if the copy texture is still modified.
// It will be set as unmodified if new data is written from CPU, as the data previously in the texture will flush.
// It will also set as unmodified if a copy is deferred to it.
shouldCopy = fromHandle.Modified;
if (fromHandle._bindCount == 0)
{
// Repeat the copy in future if the bind count is greater than 0.
DeferredCopy = null;
}
}
}
else
{
// Copies happen directly when initializing a copy dependency.
// If dirty, do not copy. Its data no longer matters, and this handle should also be dirty.
// Also, only direct copy if the data in this handle is not already modified (can be set by copies from modified handles).
shouldCopy = !fromHandle.CheckDirty() && (fromHandle.Modified || !Modified);
}
if (shouldCopy)
{
Texture from = fromHandle._group.Storage;
Texture to = _group.Storage;
if (from.ScaleFactor != to.ScaleFactor)
{
to.PropagateScale(from);
}
from.HostTexture.CopyTo(
to.HostTexture,
fromHandle._firstLayer,
_firstLayer,
fromHandle._firstLevel,
_firstLevel);
if (fromHandle.Modified)
{
Modified = true;
RegisterSync(context);
}
result = true;
}
return result;
}
/// <summary>
/// Check if this handle has a dependency to a given texture group.
/// </summary>
/// <param name="group">The texture group to check for</param>
/// <returns>True if there is a dependency, false otherwise</returns>
public bool HasDependencyTo(TextureGroup group)
{
foreach (TextureDependency dep in Dependencies)
{
if (dep.Other.Handle._group == group)
{
return true;
}
}
return false;
}
/// <summary>
/// Inherit modified flags and dependencies from another texture handle.
/// </summary>
/// <param name="old">The texture handle to inherit from</param>
/// <param name="withCopies">Whether the handle should inherit copy dependencies or not</param>
public void Inherit(TextureGroupHandle old, bool withCopies)
{
Modified |= old.Modified;
if (withCopies)
{
foreach (TextureDependency dependency in old.Dependencies.ToArray())
{
CreateCopyDependency(dependency.Other.Handle);
if (dependency.Other.Handle.DeferredCopy == old)
{
dependency.Other.Handle.DeferredCopy = this;
}
}
DeferredCopy = old.DeferredCopy;
}
}
/// <summary>
/// Check if this region overlaps with another.
/// </summary>
/// <param name="address">Base address</param>
/// <param name="size">Size of the region</param>
/// <returns>True if overlapping, false otherwise</returns>
public bool OverlapsWith(int offset, int size)
{
return Offset < offset + size && offset < Offset + Size;
}
/// <summary>
/// Dispose this texture group handle, removing all its dependencies and disposing its memory tracking handles.
/// </summary>
public void Dispose()
{
foreach (CpuRegionHandle handle in Handles)
{
handle.Dispose();
}
foreach (TextureDependency dependency in Dependencies.ToArray())
{
dependency.Other.Handle.RemoveDependency(dependency.Other);
}
}
}
}