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

@ -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>