Texture/Buffer Memory Management Improvements (#1408)

* Initial implementation. Still pending better valid-overlap handling,
disposed pool, compressed format flush fix.

* Very messy backend resource cache.

* Oops

* Dispose -> Release

* Improve Release/Dispose.

* More rule refinement.

* View compatibility levels as an enum - you can always know if a view is only copy compatible.

* General cleanup.

Use locking on the resource cache, as it is likely to be used by other threads in future.

* Rename resource cache to resource pool.

* Address some of the smaller nits.

* Fix regression with MK8 lens flare

Texture flushes done the old way should trigger memory tracking.

* Use TextureCreateInfo as a key.

It now implements IEquatable and generates a hashcode based on width/height.

* Fix size change for compressed+non-compressed view combos.

Before, this could set either the compressed or non compressed texture with a size with the wrong size, depending on which texture had its size changed. This caused exceptions when flushing the texture.

Now it correctly takes the block size into account, assuming that these textures are only related because a pixel in the non-compressed texture represents a block in the compressed one.

* Implement JD's suggestion for HashCode Combine

Co-authored-by: jduncanator <1518948+jduncanator@users.noreply.github.com>

* Address feedback

* Address feedback.

Co-authored-by: jduncanator <1518948+jduncanator@users.noreply.github.com>
This commit is contained in:
riperiperi 2020-09-10 20:44:04 +01:00 committed by GitHub
parent 4c7bebf3e6
commit 5d69d9103e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 725 additions and 140 deletions

View file

@ -145,10 +145,36 @@ namespace Ryujinx.Cpu
return;
}
MarkRegionAsModified(va, (ulong)data.Length);
WriteImpl(va, data);
}
/// <summary>
/// Writes data to CPU mapped memory, without tracking.
/// </summary>
/// <param name="va">Virtual address to write the data into</param>
/// <param name="data">Data to be written</param>
public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return;
}
WriteImpl(va, data);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <summary>
/// Writes data to CPU mapped memory.
/// </summary>
/// <param name="va">Virtual address to write the data into</param>
/// <param name="data">Data to be written</param>
private void WriteImpl(ulong va, ReadOnlySpan<byte> data)
{
try
{
MarkRegionAsModified(va, (ulong)data.Length);
if (IsContiguousAndMapped(va, data.Length))
{
data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length));

View file

@ -29,6 +29,8 @@ namespace Ryujinx.Graphics.GAL
void UpdateCounters();
void PreFrame();
ICounterEvent ReportCounter(CounterType type, EventHandler<ulong> resultHandler);
void ResetCounter(CounterType type);

View file

@ -2,7 +2,7 @@ using System;
namespace Ryujinx.Graphics.GAL
{
public interface ITexture : IDisposable
public interface ITexture
{
int Width { get; }
int Height { get; }
@ -17,5 +17,6 @@ namespace Ryujinx.Graphics.GAL
void SetData(ReadOnlySpan<byte> data);
void SetStorage(BufferRange buffer);
void Release();
}
}

View file

@ -3,7 +3,7 @@ using System;
namespace Ryujinx.Graphics.GAL
{
public struct TextureCreateInfo
public struct TextureCreateInfo : IEquatable<TextureCreateInfo>
{
public int Width { get; }
public int Height { get; }
@ -116,5 +116,29 @@ namespace Ryujinx.Graphics.GAL
{
return Math.Max(1, size >> level);
}
public override int GetHashCode()
{
return HashCode.Combine(Width, Height);
}
bool IEquatable<TextureCreateInfo>.Equals(TextureCreateInfo other)
{
return Width == other.Width &&
Height == other.Height &&
Depth == other.Depth &&
Levels == other.Levels &&
Samples == other.Samples &&
BlockWidth == other.BlockWidth &&
BlockHeight == other.BlockHeight &&
BytesPerPixel == other.BytesPerPixel &&
Format == other.Format &&
DepthStencilMode == other.DepthStencilMode &&
Target == other.Target &&
SwizzleR == other.SwizzleR &&
SwizzleG == other.SwizzleG &&
SwizzleB == other.SwizzleB &&
SwizzleA == other.SwizzleA;
}
}
}

View file

@ -58,6 +58,7 @@ namespace Ryujinx.Graphics.Gpu.Engine
TextureManager = new TextureManager(context);
context.MemoryManager.MemoryUnmapped += _counterCache.MemoryUnmappedHandler;
context.MemoryManager.MemoryUnmapped += TextureManager.MemoryUnmappedHandler;
}
/// <summary>

View file

@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using System.Collections;
using System.Collections.Generic;
@ -40,6 +41,16 @@ namespace Ryujinx.Graphics.Gpu.Image
{
Texture oldestTexture = _textures.First.Value;
oldestTexture.SynchronizeMemory();
if (oldestTexture.IsModified)
{
// The texture must be flushed if it falls out of the auto delete cache.
// Flushes out of the auto delete cache do not trigger write tracking,
// as it is expected that other overlapping textures exist that have more up-to-date contents.
oldestTexture.Flush(false);
}
_textures.RemoveFirst();
oldestTexture.DecrementReferenceCount();
@ -74,6 +85,26 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
public bool Remove(Texture texture, bool flush)
{
if (texture.CacheNode == null)
{
return false;
}
// Remove our reference to this texture.
if (flush && texture.IsModified)
{
texture.Flush(false);
}
_textures.Remove(texture.CacheNode);
texture.CacheNode = null;
return texture.DecrementReferenceCount();
}
public IEnumerator<Texture> GetEnumerator()
{
return _textures.GetEnumerator();

View file

@ -39,6 +39,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public TextureScaleMode ScaleMode { get; private set; }
/// <summary>
/// Set when a texture has been modified since it was last flushed.
/// </summary>
public bool IsModified { get; internal set; }
private bool _everModified;
private int _depth;
private int _layers;
private int _firstLayer;
@ -121,7 +128,7 @@ namespace Ryujinx.Graphics.Gpu.Image
ScaleFactor = scaleFactor;
ScaleMode = scaleMode;
_hasData = true;
InitializeData(true);
}
/// <summary>
@ -137,16 +144,6 @@ namespace Ryujinx.Graphics.Gpu.Image
ScaleMode = scaleMode;
InitializeTexture(context, info, sizeInfo);
TextureCreateInfo createInfo = TextureManager.GetCreateInfo(info, context.Capabilities);
HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
if (scaleMode == TextureScaleMode.Scaled)
{
SynchronizeMemory(); // Load the data and then scale it up.
SetScale(GraphicsConfig.ResScale);
}
}
/// <summary>
@ -171,6 +168,47 @@ namespace Ryujinx.Graphics.Gpu.Image
_views = new List<Texture>();
}
/// <summary>
/// Initializes the data for a texture. Can optionally initialize the texture with or without data.
/// If the texture is a view, it will initialize memory tracking to be non-dirty.
/// </summary>
/// <param name="isView">True if the texture is a view, false otherwise</param>
/// <param name="withData">True if the texture is to be initialized with data</param>
public void InitializeData(bool isView, bool withData = false)
{
if (withData)
{
Debug.Assert(!isView);
TextureCreateInfo createInfo = TextureManager.GetCreateInfo(Info, _context.Capabilities);
HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
SynchronizeMemory(); // Load the data.
if (ScaleMode == TextureScaleMode.Scaled)
{
SetScale(GraphicsConfig.ResScale); // Scale the data up.
}
}
else
{
// Don't update this texture the next time we synchronize.
ConsumeModified();
_hasData = true;
if (!isView)
{
if (ScaleMode == TextureScaleMode.Scaled)
{
// Don't need to start at 1x as there is no data to scale, just go straight to the target scale.
ScaleFactor = GraphicsConfig.ResScale;
}
TextureCreateInfo createInfo = TextureManager.GetCreateInfo(Info, _context.Capabilities);
HostTexture = _context.Renderer.CreateTexture(createInfo, ScaleFactor);
}
}
}
/// <summary>
/// Create a texture view from this texture.
/// A texture view is defined as a child texture, from a sub-range of their parent texture.
@ -238,6 +276,9 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="depthOrLayers">The new texture depth (for 3D textures) or layers (for layered textures)</param>
public void ChangeSize(int width, int height, int depthOrLayers)
{
int blockWidth = Info.FormatInfo.BlockWidth;
int blockHeight = Info.FormatInfo.BlockHeight;
width <<= _firstLevel;
height <<= _firstLevel;
@ -250,7 +291,7 @@ namespace Ryujinx.Graphics.Gpu.Image
depthOrLayers = _viewStorage.Info.DepthOrLayers;
}
_viewStorage.RecreateStorageOrView(width, height, depthOrLayers);
_viewStorage.RecreateStorageOrView(width, height, blockWidth, blockHeight, depthOrLayers);
foreach (Texture view in _viewStorage._views)
{
@ -268,10 +309,28 @@ namespace Ryujinx.Graphics.Gpu.Image
viewDepthOrLayers = view.Info.DepthOrLayers;
}
view.RecreateStorageOrView(viewWidth, viewHeight, viewDepthOrLayers);
view.RecreateStorageOrView(viewWidth, viewHeight, blockWidth, blockHeight, viewDepthOrLayers);
}
}
/// <summary>
/// Recreates the texture storage (or view, in the case of child textures) of this texture.
/// This allows recreating the texture with a new size.
/// A copy is automatically performed from the old to the new texture.
/// </summary>
/// <param name="width">The new texture width</param>
/// <param name="height">The new texture height</param>
/// <param name="width">The block width related to the given width</param>
/// <param name="height">The block height related to the given height</param>
/// <param name="depthOrLayers">The new texture depth (for 3D textures) or layers (for layered textures)</param>
private void RecreateStorageOrView(int width, int height, int blockWidth, int blockHeight, int depthOrLayers)
{
RecreateStorageOrView(
BitUtils.DivRoundUp(width * Info.FormatInfo.BlockWidth, blockWidth),
BitUtils.DivRoundUp(height * Info.FormatInfo.BlockHeight, blockHeight),
depthOrLayers);
}
/// <summary>
/// Recreates the texture storage (or view, in the case of child textures) of this texture.
/// This allows recreating the texture with a new size.
@ -388,8 +447,8 @@ namespace Ryujinx.Graphics.Gpu.Image
from.CopyTo(to, new Extents2D(0, 0, from.Width, from.Height), new Extents2D(0, 0, to.Width, to.Height), true);
from.Dispose();
to.Dispose();
from.Release();
to.Release();
}
}
@ -462,6 +521,16 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// Checks if the memory for this texture was modified, and returns true if it was.
/// The modified flags are consumed as a result.
/// </summary>
/// <returns>True if the texture was modified, false otherwise.</returns>
public bool ConsumeModified()
{
return _context.PhysicalMemory.QueryModified(Address, Size, ResourceName.Texture, _modifiedRanges) > 0;
}
/// <summary>
/// Synchronizes guest and host memory.
/// This will overwrite the texture data with the texture data on the guest memory, if a CPU
@ -494,15 +563,15 @@ namespace Ryujinx.Graphics.Gpu.Image
ReadOnlySpan<byte> data = _context.PhysicalMemory.GetSpan(Address, (int)Size);
// If the texture was modified by the host GPU, we do partial invalidation
// If the texture was ever modified by the host GPU, we do partial invalidation
// of the texture by getting GPU data and merging in the pages of memory
// that were modified.
// Note that if ASTC is not supported by the GPU we can't read it back since
// it will use a different format. Since applications shouldn't be writing
// ASTC textures from the GPU anyway, ignoring it should be safe.
if (_context.Methods.TextureManager.IsTextureModified(this) && !Info.FormatInfo.Format.IsAstc())
if (_everModified && !Info.FormatInfo.Format.IsAstc())
{
Span<byte> gpuData = GetTextureDataFromGpu();
Span<byte> gpuData = GetTextureDataFromGpu(true);
ulong endAddress = Address + Size;
@ -533,6 +602,8 @@ namespace Ryujinx.Graphics.Gpu.Image
data = gpuData;
}
IsModified = false;
data = ConvertToHostCompatibleFormat(data);
HostTexture.SetData(data);
@ -607,10 +678,24 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Be aware that this is an expensive operation, avoid calling it unless strictly needed.
/// This may cause data corruption if the memory is already being used for something else on the CPU side.
/// </summary>
public void Flush()
/// <param name="tracked">Whether or not the flush triggers write tracking. If it doesn't, the texture will not be blacklisted for scaling either.</param>
public void Flush(bool tracked = true)
{
BlacklistScale();
_context.PhysicalMemory.Write(Address, GetTextureDataFromGpu());
IsModified = false;
if (Info.FormatInfo.Format.IsAstc())
{
return; // Flushing this format is not supported, as it may have been converted to another host format.
}
if (tracked)
{
_context.PhysicalMemory.Write(Address, GetTextureDataFromGpu(tracked));
}
else
{
_context.PhysicalMemory.WriteUntracked(Address, GetTextureDataFromGpu(tracked));
}
}
/// <summary>
@ -621,10 +706,26 @@ namespace Ryujinx.Graphics.Gpu.Image
/// This is not cheap, avoid doing that unless strictly needed.
/// </remarks>
/// <returns>Host texture data</returns>
private Span<byte> GetTextureDataFromGpu()
private Span<byte> GetTextureDataFromGpu(bool blacklist)
{
BlacklistScale();
Span<byte> data = HostTexture.GetData();
Span<byte> data;
if (blacklist)
{
BlacklistScale();
data = HostTexture.GetData();
}
else if (ScaleFactor != 1f)
{
float scale = ScaleFactor;
SetScale(1f);
data = HostTexture.GetData();
SetScale(scale);
}
else
{
data = HostTexture.GetData();
}
if (Info.IsLinear)
{
@ -713,31 +814,12 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="size">Texture view size</param>
/// <param name="firstLayer">Texture view initial layer on this texture</param>
/// <param name="firstLevel">Texture view first mipmap level on this texture</param>
/// <returns>True if a view with the given parameters can be created from this texture, false otherwise</returns>
public bool IsViewCompatible(
/// <returns>The level of compatiblilty a view with the given parameters created from this texture has</returns>
public TextureViewCompatibility IsViewCompatible(
TextureInfo info,
ulong size,
out int firstLayer,
out int firstLevel)
{
return IsViewCompatible(info, size, isCopy: false, out firstLayer, out firstLevel);
}
/// <summary>
/// Check if it's possible to create a view, with the given parameters, from this texture.
/// </summary>
/// <param name="info">Texture view information</param>
/// <param name="size">Texture view size</param>
/// <param name="isCopy">True to check for copy compability, instead of view compatibility</param>
/// <param name="firstLayer">Texture view initial layer on this texture</param>
/// <param name="firstLevel">Texture view first mipmap level on this texture</param>
/// <returns>True if a view with the given parameters can be created from this texture, false otherwise</returns>
public bool IsViewCompatible(
TextureInfo info,
ulong size,
bool isCopy,
out int firstLayer,
out int firstLevel)
{
// Out of range.
if (info.Address < Address || info.Address + size > EndAddress)
@ -745,38 +827,46 @@ namespace Ryujinx.Graphics.Gpu.Image
firstLayer = 0;
firstLevel = 0;
return false;
return TextureViewCompatibility.Incompatible;
}
int offset = (int)(info.Address - Address);
if (!_sizeInfo.FindView(offset, (int)size, out firstLayer, out firstLevel))
{
return false;
return TextureViewCompatibility.Incompatible;
}
if (!TextureCompatibility.ViewLayoutCompatible(Info, info, firstLevel))
{
return false;
return TextureViewCompatibility.Incompatible;
}
if (!TextureCompatibility.ViewFormatCompatible(Info, info))
{
return false;
return TextureViewCompatibility.Incompatible;
}
if (!TextureCompatibility.ViewSizeMatches(Info, info, firstLevel, isCopy))
{
return false;
}
TextureViewCompatibility result = TextureViewCompatibility.Full;
if (!TextureCompatibility.ViewTargetCompatible(Info, info, isCopy))
{
return false;
}
result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewSizeMatches(Info, info, firstLevel));
result = TextureCompatibility.PropagateViewCompatibility(result, TextureCompatibility.ViewTargetCompatible(Info, info));
return Info.SamplesInX == info.SamplesInX &&
Info.SamplesInY == info.SamplesInY;
return (Info.SamplesInX == info.SamplesInX &&
Info.SamplesInY == info.SamplesInY) ? result : TextureViewCompatibility.Incompatible;
}
/// <summary>
/// Checks if the view format is compatible with this texture format.
/// In general, the formats are considered compatible if the bytes per pixel values are equal,
/// but there are more complex rules for some formats, like compressed or depth-stencil formats.
/// This follows the host API copy compatibility rules.
/// </summary>
/// <param name="info">Texture information of the texture view</param>
/// <returns>True if the formats are compatible, false otherwise</returns>
private bool ViewFormatCompatible(TextureInfo info)
{
return TextureCompatibility.FormatCompatible(Info.FormatInfo, info.FormatInfo);
}
/// <summary>
@ -902,7 +992,15 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public void SignalModified()
{
IsModified = true;
_everModified = true;
Modified?.Invoke(this);
if (_viewStorage != this)
{
_viewStorage.SignalModified();
}
}
/// <summary>
@ -927,6 +1025,29 @@ namespace Ryujinx.Graphics.Gpu.Image
return Address < address + size && address < EndAddress;
}
/// <summary>
/// Determine if any of our child textures are compaible as views of the given texture.
/// </summary>
/// <param name="texture">The texture to check against</param>
/// <returns>True if any child is view compatible, false otherwise</returns>
public bool HasViewCompatibleChild(Texture texture)
{
if (_viewStorage != this || _views.Count == 0)
{
return false;
}
foreach (Texture view in _views)
{
if (texture.IsViewCompatible(view.Info, view.Size, out int _, out int _) != TextureViewCompatibility.Incompatible)
{
return true;
}
}
return false;
}
/// <summary>
/// Increments the texture reference count.
/// </summary>
@ -939,7 +1060,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Decrements the texture reference count.
/// When the reference count hits zero, the texture may be deleted and can't be used anymore.
/// </summary>
public void DecrementReferenceCount()
/// <returns>True if the texture is now referenceless, false otherwise</returns>
public bool DecrementReferenceCount()
{
int newRefCount = --_referenceCount;
@ -956,6 +1078,8 @@ namespace Ryujinx.Graphics.Gpu.Image
Debug.Assert(newRefCount >= 0);
DeleteIfNotUsed();
return newRefCount <= 0;
}
/// <summary>
@ -980,12 +1104,21 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
private void DisposeTextures()
{
HostTexture.Dispose();
HostTexture.Release();
_arrayViewTexture?.Dispose();
_arrayViewTexture?.Release();
_arrayViewTexture = null;
}
/// <summary>
/// Called when the memory for this texture has been unmapped.
/// Calls are from non-gpu threads.
/// </summary>
public void Unmapped()
{
IsModified = false; // We shouldn't flush this texture, as its memory is no longer mapped.
}
/// <summary>
/// Performs texture disposal, deleting the texture.
/// </summary>

View file

@ -147,28 +147,51 @@ namespace Ryujinx.Graphics.Gpu.Image
}
/// <summary>
/// Checks if the view sizes of a two given texture informations match.
/// Obtain the minimum compatibility level of two provided view compatibility results.
/// </summary>
/// <param name="first">The first compatibility level</param>
/// <param name="second">The second compatibility level</param>
/// <returns>The minimum compatibility level of two provided view compatibility results</returns>
public static TextureViewCompatibility PropagateViewCompatibility(TextureViewCompatibility first, TextureViewCompatibility second)
{
if (first == TextureViewCompatibility.Incompatible || second == TextureViewCompatibility.Incompatible)
{
return TextureViewCompatibility.Incompatible;
}
else if (first == TextureViewCompatibility.CopyOnly || second == TextureViewCompatibility.CopyOnly)
{
return TextureViewCompatibility.CopyOnly;
}
else
{
return TextureViewCompatibility.Full;
}
}
/// <summary>
/// Checks if the sizes of two given textures are view compatible.
/// </summary>
/// <param name="lhs">Texture information of the texture view</param>
/// <param name="rhs">Texture information of the texture view to match against</param>
/// <param name="level">Mipmap level of the texture view in relation to this texture</param>
/// <param name="isCopy">True to check for copy compatibility rather than view compatibility</param>
/// <returns>True if the sizes are compatible, false otherwise</returns>
public static bool ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level, bool isCopy)
public static TextureViewCompatibility ViewSizeMatches(TextureInfo lhs, TextureInfo rhs, int level)
{
Size size = GetAlignedSize(lhs, level);
Size otherSize = GetAlignedSize(rhs);
TextureViewCompatibility result = TextureViewCompatibility.Full;
// For copies, we can copy a subset of the 3D texture slices,
// so the depth may be different in this case.
if (!isCopy && rhs.Target == Target.Texture3D && size.Depth != otherSize.Depth)
if (rhs.Target == Target.Texture3D && size.Depth != otherSize.Depth)
{
return false;
result = TextureViewCompatibility.CopyOnly;
}
return size.Width == otherSize.Width &&
size.Height == otherSize.Height;
return (size.Width == otherSize.Width &&
size.Height == otherSize.Height) ? result : TextureViewCompatibility.Incompatible;
}
/// <summary>
@ -330,38 +353,48 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="rhs">Texture information of the texture view</param>
/// <param name="isCopy">True to check for copy rather than view compatibility</param>
/// <returns>True if the targets are compatible, false otherwise</returns>
public static bool ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs, bool isCopy)
public static TextureViewCompatibility ViewTargetCompatible(TextureInfo lhs, TextureInfo rhs)
{
bool result = false;
switch (lhs.Target)
{
case Target.Texture1D:
case Target.Texture1DArray:
return rhs.Target == Target.Texture1D ||
rhs.Target == Target.Texture1DArray;
result = rhs.Target == Target.Texture1D ||
rhs.Target == Target.Texture1DArray;
break;
case Target.Texture2D:
return rhs.Target == Target.Texture2D ||
rhs.Target == Target.Texture2DArray;
result = rhs.Target == Target.Texture2D ||
rhs.Target == Target.Texture2DArray;
break;
case Target.Texture2DArray:
case Target.Cubemap:
case Target.CubemapArray:
return rhs.Target == Target.Texture2D ||
rhs.Target == Target.Texture2DArray ||
rhs.Target == Target.Cubemap ||
rhs.Target == Target.CubemapArray;
result = rhs.Target == Target.Texture2D ||
rhs.Target == Target.Texture2DArray ||
rhs.Target == Target.Cubemap ||
rhs.Target == Target.CubemapArray;
break;
case Target.Texture2DMultisample:
case Target.Texture2DMultisampleArray:
return rhs.Target == Target.Texture2DMultisample ||
rhs.Target == Target.Texture2DMultisampleArray;
result = rhs.Target == Target.Texture2DMultisample ||
rhs.Target == Target.Texture2DMultisampleArray;
break;
case Target.Texture3D:
return rhs.Target == Target.Texture3D ||
(rhs.Target == Target.Texture2D && isCopy);
if (rhs.Target == Target.Texture2D)
{
return TextureViewCompatibility.CopyOnly;
}
result = rhs.Target == Target.Texture3D;
break;
}
return false;
return result ? TextureViewCompatibility.Full : TextureViewCompatibility.Incompatible;
}
/// <summary>

View file

@ -14,6 +14,20 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
class TextureManager : IDisposable
{
private struct OverlapInfo
{
public TextureViewCompatibility Compatibility { get; }
public int FirstLayer { get; }
public int FirstLevel { get; }
public OverlapInfo(TextureViewCompatibility compatibility, int firstLayer, int firstLevel)
{
Compatibility = compatibility;
FirstLayer = firstLayer;
FirstLevel = firstLevel;
}
}
private const int OverlapsBufferInitialCapacity = 10;
private const int OverlapsBufferMaxCapacity = 10000;
@ -33,6 +47,7 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly RangeList<Texture> _textures;
private Texture[] _textureOverlaps;
private OverlapInfo[] _overlapInfo;
private readonly AutoDeleteCache _cache;
@ -64,6 +79,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_textures = new RangeList<Texture>();
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
_cache = new AutoDeleteCache();
@ -407,6 +423,27 @@ namespace Ryujinx.Graphics.Gpu.Image
return true;
}
/// <summary>
/// Handles removal of textures written to a memory region being unmapped.
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="e">Event arguments</param>
public void MemoryUnmappedHandler(object sender, UnmapEventArgs e)
{
Texture[] overlaps = new Texture[10];
int overlapCount;
lock (_textures)
{
overlapCount = _textures.FindOverlaps(_context.MemoryManager.Translate(e.Address), e.Size, ref overlaps);
}
for (int i = 0; i < overlapCount; i++)
{
overlaps[i].Unmapped();
}
}
/// <summary>
/// Tries to find an existing texture, or create a new one if not found.
/// </summary>
@ -618,8 +655,13 @@ namespace Ryujinx.Graphics.Gpu.Image
scaleMode = (flags & TextureSearchFlags.WithUpscale) != 0 ? TextureScaleMode.Scaled : TextureScaleMode.Eligible;
}
// Try to find a perfect texture match, with the same address and parameters.
int sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps);
int sameAddressOverlapsCount;
lock (_textures)
{
// Try to find a perfect texture match, with the same address and parameters.
sameAddressOverlapsCount = _textures.FindOverlaps(info.Address, ref _textureOverlaps);
}
for (int index = 0; index < sameAddressOverlapsCount; index++)
{
@ -681,8 +723,12 @@ namespace Ryujinx.Graphics.Gpu.Image
// Find view compatible matches.
ulong size = (ulong)sizeInfo.TotalSize;
int overlapsCount;
int overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps);
lock (_textures)
{
overlapsCount = _textures.FindOverlaps(info.Address, size, ref _textureOverlaps);
}
Texture texture = null;
@ -690,7 +736,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
Texture overlap = _textureOverlaps[index];
if (overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel))
if (overlap.IsViewCompatible(info, size, out int firstLayer, out int firstLevel) == TextureViewCompatibility.Full)
{
if (!isSamplerTexture)
{
@ -701,7 +747,7 @@ namespace Ryujinx.Graphics.Gpu.Image
if (IsTextureModified(overlap))
{
CacheTextureModified(texture);
texture.SignalModified();
}
// The size only matters (and is only really reliable) when the
@ -721,65 +767,114 @@ namespace Ryujinx.Graphics.Gpu.Image
{
texture = new Texture(_context, info, sizeInfo, scaleMode);
// We need to synchronize before copying the old view data to the texture,
// otherwise the copied data would be overwritten by a future synchronization.
texture.SynchronizeMemory();
// Step 1: Find textures that are view compatible with the new texture.
// Any textures that are incompatible will contain garbage data, so they should be removed where possible.
int viewCompatible = 0;
bool setData = isSamplerTexture || overlapsCount == 0;
for (int index = 0; index < overlapsCount; index++)
{
Texture overlap = _textureOverlaps[index];
bool overlapInCache = overlap.CacheNode != null;
if (texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel))
TextureViewCompatibility compatibility = texture.IsViewCompatible(overlap.Info, overlap.Size, out int firstLayer, out int firstLevel);
if (compatibility != TextureViewCompatibility.Incompatible)
{
TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, firstLevel);
TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities);
if (texture.ScaleFactor != overlap.ScaleFactor)
if (_overlapInfo.Length != _textureOverlaps.Length)
{
// A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself.
// In that case, we prefer the higher scale only if our format is render-target-like, otherwise we scale the view down before copy.
texture.PropagateScale(overlap);
Array.Resize(ref _overlapInfo, _textureOverlaps.Length);
}
ITexture newView = texture.HostTexture.CreateView(createInfo, firstLayer, firstLevel);
overlap.HostTexture.CopyTo(newView, 0, 0);
// Inherit modification from overlapping texture, do that before replacing
// the view since the replacement operation removes it from the list.
if (IsTextureModified(overlap))
{
CacheTextureModified(texture);
}
overlap.ReplaceView(texture, overlapInfo, newView, firstLayer, firstLevel);
_overlapInfo[viewCompatible] = new OverlapInfo(compatibility, firstLayer, firstLevel);
_textureOverlaps[viewCompatible++] = overlap;
}
else if (overlapInCache || !setData)
{
if (info.GobBlocksInZ > 1 && info.GobBlocksInZ == overlap.Info.GobBlocksInZ)
{
// Allow overlapping slices of 3D textures. Could be improved in future by making sure the textures don't overlap.
continue;
}
// The overlap texture is going to contain garbage data after we draw, or is generally incompatible.
// If the texture cannot be entirely contained in the new address space, and one of its view children is compatible with us,
// it must be flushed before removal, so that the data is not lost.
// If the texture was modified since its last use, then that data is probably meant to go into this texture.
// If the data has been modified by the CPU, then it also shouldn't be flushed.
bool modified = overlap.ConsumeModified();
bool flush = overlapInCache && !modified && (overlap.Address < texture.Address || overlap.EndAddress > texture.EndAddress) && overlap.HasViewCompatibleChild(texture);
setData |= modified || flush;
if (overlapInCache)
{
_cache.Remove(overlap, flush);
}
}
}
// We need to synchronize before copying the old view data to the texture,
// otherwise the copied data would be overwritten by a future synchronization.
texture.InitializeData(false, setData);
for (int index = 0; index < viewCompatible; index++)
{
Texture overlap = _textureOverlaps[index];
OverlapInfo oInfo = _overlapInfo[index];
if (oInfo.Compatibility != TextureViewCompatibility.Full)
{
continue; // Copy only compatibilty.
}
TextureInfo overlapInfo = AdjustSizes(texture, overlap.Info, oInfo.FirstLevel);
TextureCreateInfo createInfo = GetCreateInfo(overlapInfo, _context.Capabilities);
if (texture.ScaleFactor != overlap.ScaleFactor)
{
// A bit tricky, our new texture may need to contain an existing texture that is upscaled, but isn't itself.
// In that case, we prefer the higher scale only if our format is render-target-like, otherwise we scale the view down before copy.
texture.PropagateScale(overlap);
}
ITexture newView = texture.HostTexture.CreateView(createInfo, oInfo.FirstLayer, oInfo.FirstLevel);
overlap.HostTexture.CopyTo(newView, 0, 0);
// Inherit modification from overlapping texture, do that before replacing
// the view since the replacement operation removes it from the list.
if (IsTextureModified(overlap))
{
texture.SignalModified();
}
overlap.ReplaceView(texture, overlapInfo, newView, oInfo.FirstLayer, oInfo.FirstLevel);
}
// If the texture is a 3D texture, we need to additionally copy any slice
// of the 3D texture to the newly created 3D texture.
if (info.Target == Target.Texture3D)
{
for (int index = 0; index < overlapsCount; index++)
for (int index = 0; index < viewCompatible; index++)
{
Texture overlap = _textureOverlaps[index];
OverlapInfo oInfo = _overlapInfo[index];
if (texture.IsViewCompatible(
overlap.Info,
overlap.Size,
isCopy: true,
out int firstLayer,
out int firstLevel))
if (oInfo.Compatibility != TextureViewCompatibility.Incompatible)
{
overlap.BlacklistScale();
overlap.HostTexture.CopyTo(texture.HostTexture, firstLayer, firstLevel);
overlap.HostTexture.CopyTo(texture.HostTexture, oInfo.FirstLayer, oInfo.FirstLevel);
if (IsTextureModified(overlap))
{
CacheTextureModified(texture);
texture.SignalModified();
}
}
}
@ -795,7 +890,10 @@ namespace Ryujinx.Graphics.Gpu.Image
texture.Disposed += CacheTextureDisposed;
}
_textures.Add(texture);
lock (_textures)
{
_textures.Add(texture);
}
ShrinkOverlapsBufferIfNeeded();
@ -818,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="texture">The texture that was modified.</param>
private void CacheTextureModified(Texture texture)
{
texture.IsModified = true;
_modified.Add(texture);
if (texture.Info.IsLinear)
@ -992,7 +1091,10 @@ namespace Ryujinx.Graphics.Gpu.Image
{
foreach (Texture texture in _modifiedLinear)
{
texture.Flush();
if (texture.IsModified)
{
texture.Flush();
}
}
_modifiedLinear.Clear();
@ -1007,7 +1109,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
foreach (Texture texture in _modified)
{
if (texture.OverlapsWith(address, size))
if (texture.OverlapsWith(address, size) && texture.IsModified)
{
texture.Flush();
}
@ -1024,7 +1126,10 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="texture">The texture to be removed</param>
public void RemoveTextureFromCache(Texture texture)
{
_textures.Remove(texture);
lock (_textures)
{
_textures.Remove(texture);
}
}
/// <summary>
@ -1033,10 +1138,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public void Dispose()
{
foreach (Texture texture in _textures)
lock (_textures)
{
_modified.Remove(texture);
texture.Dispose();
foreach (Texture texture in _textures)
{
_modified.Remove(texture);
texture.Dispose();
}
}
}
}

View file

@ -0,0 +1,13 @@
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// The level of view compatibility one texture has to another.
/// Values are increasing in compatibility from 0 (incompatible).
/// </summary>
enum TextureViewCompatibility
{
Incompatible = 0,
CopyOnly,
Full
}
}

View file

@ -151,7 +151,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
byte[] data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
_context.PhysicalMemory.Write(address, data);
// TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers.
_context.PhysicalMemory.WriteUntracked(address, data);
}
/// <summary>

View file

@ -67,6 +67,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
_cpuMemory.Write(address, data);
}
/// <summary>
/// Writes data to the application process, without any tracking.
/// </summary>
/// <param name="address">Address to write into</param>
/// <param name="data">Data to be written</param>
public void WriteUntracked(ulong address, ReadOnlySpan<byte> data)
{
_cpuMemory.WriteUntracked(address, data);
}
/// <summary>
/// Checks if a specified virtual memory region has been modified by the CPU since the last call.
/// </summary>

View file

@ -67,5 +67,10 @@ namespace Ryujinx.Graphics.OpenGL.Image
Handle = 0;
}
}
public void Release()
{
Dispose();
}
}
}

View file

@ -16,6 +16,8 @@ namespace Ryujinx.Graphics.OpenGL.Image
private int _viewsCount;
internal ITexture DefaultView { get; private set; }
public TextureStorage(Renderer renderer, TextureCreateInfo info, float scaleFactor)
{
_renderer = renderer;
@ -147,7 +149,9 @@ namespace Ryujinx.Graphics.OpenGL.Image
public ITexture CreateDefaultView()
{
return CreateView(Info, 0, 0);
DefaultView = CreateView(Info, 0, 0);
return DefaultView;
}
public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel)
@ -167,12 +171,37 @@ namespace Ryujinx.Graphics.OpenGL.Image
// If we don't have any views, then the storage is now useless, delete it.
if (--_viewsCount == 0)
{
Dispose();
if (DefaultView == null)
{
Dispose();
}
else
{
// If the default view still exists, we can put it into the resource pool.
Release();
}
}
}
/// <summary>
/// Release the TextureStorage to the resource pool without disposing its handle.
/// </summary>
public void Release()
{
_viewsCount = 1; // When we are used again, we will have the default view.
_renderer.ResourcePool.AddTexture((TextureView)DefaultView);
}
public void DeleteDefault()
{
DefaultView = null;
}
public void Dispose()
{
DefaultView = null;
if (Handle != 0)
{
GL.DeleteTexture(Handle);

View file

@ -434,7 +434,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
throw new NotSupportedException();
}
public void Dispose()
private void DisposeHandles()
{
if (_incompatibleFormatView != null)
{
@ -447,10 +447,38 @@ namespace Ryujinx.Graphics.OpenGL.Image
{
GL.DeleteTexture(Handle);
_parent.DecrementViewsCount();
Handle = 0;
}
}
/// <summary>
/// Release the view without necessarily disposing the parent if we are the default view.
/// This allows it to be added to the resource pool and reused later.
/// </summary>
public void Release()
{
bool hadHandle = Handle != 0;
if (_parent.DefaultView != this)
{
DisposeHandles();
}
if (hadHandle)
{
_parent.DecrementViewsCount();
}
}
public void Dispose()
{
if (_parent.DefaultView == this)
{
// Remove the default view (us), so that the texture cannot be released to the cache.
_parent.DeleteDefault();
}
Release();
}
}
}

View file

@ -23,6 +23,8 @@ namespace Ryujinx.Graphics.OpenGL
internal TextureCopy TextureCopy { get; }
internal ResourcePool ResourcePool { get; }
public string GpuVendor { get; private set; }
public string GpuRenderer { get; private set; }
public string GpuVersion { get; private set; }
@ -33,6 +35,7 @@ namespace Ryujinx.Graphics.OpenGL
_counters = new Counters();
_window = new Window(this);
TextureCopy = new TextureCopy(this);
ResourcePool = new ResourcePool();
}
public IShader CompileShader(ShaderProgram shader)
@ -57,7 +60,14 @@ namespace Ryujinx.Graphics.OpenGL
public ITexture CreateTexture(TextureCreateInfo info, float scaleFactor)
{
return info.Target == Target.TextureBuffer ? new TextureBuffer(info) : new TextureStorage(this, info, scaleFactor).CreateDefaultView();
if (info.Target == Target.TextureBuffer)
{
return new TextureBuffer(info);
}
else
{
return ResourcePool.GetTextureOrNull(info, scaleFactor) ?? new TextureStorage(this, info, scaleFactor).CreateDefaultView();
}
}
public void DeleteBuffer(BufferHandle buffer)
@ -92,6 +102,11 @@ namespace Ryujinx.Graphics.OpenGL
_counters.Update();
}
public void PreFrame()
{
ResourcePool.Tick();
}
public ICounterEvent ReportCounter(CounterType type, EventHandler<ulong> resultHandler)
{
return _counters.QueueReport(type, resultHandler);
@ -123,6 +138,7 @@ namespace Ryujinx.Graphics.OpenGL
public void Dispose()
{
TextureCopy.Dispose();
ResourcePool.Dispose();
_pipeline.Dispose();
_window.Dispose();
_counters.Dispose();

View file

@ -0,0 +1,122 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.OpenGL.Image;
using System;
using System.Collections.Generic;
namespace Ryujinx.Graphics.OpenGL
{
class DisposedTexture
{
public TextureCreateInfo Info;
public TextureView View;
public float ScaleFactor;
public int RemainingFrames;
}
/// <summary>
/// A structure for pooling resources that can be reused without recreation, such as textures.
/// </summary>
class ResourcePool : IDisposable
{
private const int DisposedLiveFrames = 2;
private readonly object _lock = new object();
private readonly Dictionary<TextureCreateInfo, List<DisposedTexture>> _textures = new Dictionary<TextureCreateInfo, List<DisposedTexture>>();
/// <summary>
/// Add a texture that is not being used anymore to the resource pool to be used later.
/// Both the texture's view and storage should be completely unused.
/// </summary>
/// <param name="view">The texture's view</param>
public void AddTexture(TextureView view)
{
lock (_lock)
{
List<DisposedTexture> list;
if (!_textures.TryGetValue(view.Info, out list))
{
list = new List<DisposedTexture>();
_textures.Add(view.Info, list);
}
list.Add(new DisposedTexture()
{
Info = view.Info,
View = view,
ScaleFactor = view.ScaleFactor,
RemainingFrames = DisposedLiveFrames
});
}
}
/// <summary>
/// Attempt to obtain a texture from the resource cache with the desired parameters.
/// </summary>
/// <param name="info">The creation info for the desired texture</param>
/// <param name="scaleFactor">The scale factor for the desired texture</param>
/// <returns>A TextureView with the description specified, or null if one was not found.</returns>
public TextureView GetTextureOrNull(TextureCreateInfo info, float scaleFactor)
{
lock (_lock)
{
List<DisposedTexture> list;
if (!_textures.TryGetValue(info, out list))
{
return null;
}
foreach (DisposedTexture texture in list)
{
if (scaleFactor == texture.ScaleFactor)
{
list.Remove(texture);
return texture.View;
}
}
return null;
}
}
/// <summary>
/// Update the pool, removing any resources that have expired.
/// </summary>
public void Tick()
{
lock (_lock)
{
foreach (List<DisposedTexture> list in _textures.Values)
{
for (int i = 0; i < list.Count; i++)
{
DisposedTexture tex = list[i];
if (--tex.RemainingFrames < 0)
{
tex.View.Dispose();
list.RemoveAt(i--);
}
}
}
}
}
/// <summary>
/// Disposes the resource pool.
/// </summary>
public void Dispose()
{
lock (_lock)
{
foreach (List<DisposedTexture> list in _textures.Values)
{
foreach (DisposedTexture texture in list)
{
texture.View.Dispose();
}
}
_textures.Clear();
}
}
}
}

View file

@ -159,6 +159,8 @@ namespace Ryujinx.HLE
public void ProcessFrame()
{
Gpu.Renderer.PreFrame();
Gpu.GPFifo.DispatchCalls();
}