Texture Cache: "Texture Groups" and "Texture Dependencies" (#2001)

* Initial implementation (3d tex mips broken)

This works rather well for most games, just need to fix 3d texture mips.

* Cleanup

* Address feedback

* Copy Dependencies and various other fixes

* Fix layer/level offset for copy from view<->view.

* Remove dirty flag from dependency

The dirty flag behaviour is not needed - DeferredCopy is all we need.

* Fix tracking mip slices.

* Propagate granularity (fix astral chain)

* Address Feedback pt 1

* Save slice sizes as part of SizeInfo

* Fix nits

* Fix disposing multiple dependencies causing a crash

This list is obviously modified when removing dependencies, so create a copy of it.
This commit is contained in:
riperiperi 2021-03-02 22:30:54 +00:00 committed by GitHub
parent 7a90abc035
commit b530f0e110
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1915 additions and 220 deletions

View File

@ -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);
}
}

View File

@ -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<byte> data);
void SetData(ReadOnlySpan<byte> data, int layer, int level);
void SetStorage(BufferRange buffer);
void Release();
}

View File

@ -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<Boolean32>(MethodOffset.RtDepthStencilEnable);
@ -406,11 +401,6 @@ namespace Ryujinx.Graphics.Gpu.Engine
UpdateViewportTransform(state);
UpdateScissorState(state);
}
if (depthStencil != null)
{
depthStencil.SignalModified();
}
}
/// <summary>

View File

@ -50,6 +50,11 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
public TextureScaleMode ScaleMode { get; private set; }
/// <summary>
/// Group that this texture belongs to. Manages read/write memory tracking.
/// </summary>
public TextureGroup Group { get; private set; }
/// <summary>
/// Set when a texture has been modified by the Host GPU since it was last flushed.
/// </summary>
@ -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
/// </summary>
public MultiRange Range { get; private set; }
/// <summary>
/// Layer size in bytes.
/// </summary>
public int LayerSize => _sizeInfo.LayerSize;
/// <summary>
/// Texture size in bytes.
/// </summary>
public ulong Size => (ulong)_sizeInfo.TotalSize;
private GpuRegionHandle _memoryTracking;
/// <summary>
/// Whether or not the texture belongs is a view.
/// </summary>
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
/// <param name="withData">True if the texture is to be initialized with data</param>
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
}
}
/// <summary>
/// Initialize a new texture group with this texture as storage.
/// </summary>
/// <param name="hasLayerViews">True if the texture will have layer views</param>
/// <param name="hasMipViews">True if the texture will have mip views</param>
public void InitializeGroup(bool hasLayerViews, bool hasMipViews)
{
Group = new TextureGroup(_context, this);
Group.Initialize(ref _sizeInfo, hasLayerViews, hasMipViews);
}
/// <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.
@ -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
/// <param name="texture">The child texture</param>
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;
}
/// <summary>
@ -276,7 +316,27 @@ namespace Ryujinx.Graphics.Gpu.Image
texture._viewStorage = texture;
DeleteIfNotUsed();
DecrementReferenceCount();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="contained">The view compatible texture to create a dependency to</param>
/// <param name="layer">The base layer of the given texture relative to this one</param>
/// <param name="level">The base level of the given texture relative to this one</param>
/// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
public void CreateCopyDependency(Texture contained, int layer, int level, bool copyTo)
{
if (contained.Group == Group)
{
return;
}
Group.CreateCopyDependency(contained, FirstLayer + layer, FirstLevel + level, copyTo);
}
/// <summary>
@ -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
}
}
/// <summary>
/// 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.
/// </summary>
public void DisableMemoryTracking()
{
_memoryTracking?.Dispose();
_memoryTracking = null;
}
/// <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.
@ -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.
/// </summary>
/// <remarks>
/// If there is no memory tracking for this texture, it will always report as modified.
/// </remarks>
/// <returns>True if the texture was modified, false otherwise.</returns>
public bool ConsumeModified()
{
bool wasDirty = _memoryTracking?.Dirty ?? true;
_memoryTracking?.Reprotect();
return wasDirty;
return Group.ConsumeDirty(this);
}
/// <summary>
@ -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();
}
}
/// <summary>
/// Signal that this texture is dirty, indicating that the texture group must be checked.
/// </summary>
public void SignalGroupDirty()
{
_dirty = true;
}
/// <summary>
/// Fully synchronizes guest and host memory.
/// This will replace the entire texture with the data present in guest memory.
/// </summary>
public void SynchronizeFull()
{
if (_hasData)
{
BlacklistScale();
}
ReadOnlySpan<byte> 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;
}
/// <summary>
/// Uploads new texture data to the host GPU for a specific layer/level.
/// </summary>
/// <param name="data">New data</param>
/// <param name="layer">Target layer</param>
/// <param name="level">Target level</param>
public void SetData(ReadOnlySpan<byte> data, int layer, int level)
{
BlacklistScale();
HostTexture.SetData(data, layer, level);
_currentData = null;
_hasData = true;
}
/// <summary>
/// Converts texture data to a format and layout that is supported by the host GPU.
/// </summary>
/// <param name="data">Data to be converted</param>
/// <returns>Converted data</returns>
private ReadOnlySpan<byte> ConvertToHostCompatibleFormat(ReadOnlySpan<byte> data)
public ReadOnlySpan<byte> ConvertToHostCompatibleFormat(ReadOnlySpan<byte> 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<byte> 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
/// </summary>
public void ExternalFlush(ulong address, ulong size)
{
if (!IsModified || _memoryTracking == null)
if (!IsModified)
{
return;
}
@ -869,7 +965,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <param name="firstLayer">Texture view initial layer on this texture</param>
/// <param name="firstLevel">Texture view first mipmap level on this texture</param>
/// <returns>The level of compatiblilty a view with the given parameters created from this texture has</returns>
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
/// <param name="firstLevel">The first level of the view</param>
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();
}
/// <summary>
@ -1031,14 +1152,28 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
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);
/// <summary>
/// Signals that a texture has been bound, or has been unbound.
/// During this time, lazy copies will not clear the dirty flag.
/// </summary>
/// <param name="bound">True if the texture has been bound, false if it has been unbound</param>
public void SignalModifying(bool bound)
{
bool wasModified = IsModified;
if (!wasModified || Group.HasCopyDependencies)
{
IsModified = true;
Group.SignalModifying(this, bound, !wasModified);
}
}
/// <summary>
@ -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);
}
/// <summary>
@ -1162,7 +1293,11 @@ namespace Ryujinx.Graphics.Gpu.Image
DisposeTextures();
Disposed?.Invoke(this);
_memoryTracking?.Dispose();
if (Group.Storage == this)
{
Group.Dispose();
}
}
}
}

View File

@ -215,7 +215,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <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>
/// <returns>True if the sizes are compatible, false otherwise</returns>
/// <returns>The view compatibility level of the view sizes</returns>
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;
}
/// <summary>
/// Checks if the potential child texture fits within the level and layer bounds of the parent.
/// </summary>
/// <param name="parent">Texture information for the parent</param>
/// <param name="child">Texture information for the child</param>
/// <param name="layer">Base layer of the child texture</param>
/// <param name="level">Base level of the child texture</param>
/// <returns>Full compatiblity if the child's layer and level count fit within the parent, incompatible otherwise</returns>
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;
}
}
/// <summary>
/// Checks if the texture sizes of the supplied texture informations match.
/// </summary>
@ -382,10 +403,22 @@ namespace Ryujinx.Graphics.Gpu.Image
/// </summary>
/// <param name="lhs">Texture information of the texture view</param>
/// <param name="rhs">Texture information of the texture view</param>
/// <returns>True if the formats are compatible, false otherwise</returns>
public static bool ViewFormatCompatible(TextureInfo lhs, TextureInfo rhs)
/// <returns>The view compatibility level of the texture formats</returns>
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;
}
/// <summary>

View File

@ -0,0 +1,37 @@
namespace Ryujinx.Graphics.Gpu.Image
{
/// <summary>
/// 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.
/// </summary>
class TextureDependency
{
/// <summary>
/// The handle that owns this dependency.
/// </summary>
public TextureGroupHandle Handle;
/// <summary>
/// The other dependency linked to this one, which belongs to another handle.
/// </summary>
public TextureDependency Other;
/// <summary>
/// Create a new texture dependency.
/// </summary>
/// <param name="handle">The handle that owns the dependency</param>
public TextureDependency(TextureGroupHandle handle)
{
Handle = handle;
}
/// <summary>
/// Signal that the owner of this dependency has been modified,
/// meaning that the other dependency's handle must defer a copy from it.
/// </summary>
public void SignalModified()
{
Other.Handle.DeferCopy(Handle);
}
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
class TextureGroup : IDisposable
{
private const int StrideAlignment = 32;
private const int GobAlignment = 64;
private delegate void HandlesCallbackDelegate(int baseHandle, int regionCount, bool split = false);
/// <summary>
/// The storage texture associated with this group.
/// </summary>
public Texture Storage { get; }
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// The views list from the storage texture.
/// </summary>
private List<Texture> _views;
private TextureGroupHandle[] _handles;
private bool[] _loadNeeded;
/// <summary>
/// Create a new texture group.
/// </summary>
/// <param name="context">GPU context that the texture group belongs to</param>
/// <param name="storage">The storage texture for this group</param>
public TextureGroup(GpuContext context, Texture storage)
{
Storage = storage;
_context = context;
_is3D = storage.Info.Target == Target.Texture3D;
_layers = storage.Info.GetSlices();
_levels = storage.Info.Levels;
}
/// <summary>
/// Initialize a new texture group's dirty regions and offsets.
/// </summary>
/// <param name="size">Size info for the storage texture</param>
/// <param name="hasLayerViews">True if the storage will have layer views</param>
/// <param name="hasMipViews">True if the storage will have mip views</param>
public void Initialize(ref SizeInfo size, bool hasLayerViews, bool hasMipViews)
{
_allOffsets = size.AllOffsets;
_sliceSizes = size.SliceSizes;
(_hasLayerViews, _hasMipViews) = PropagateGranularity(hasLayerViews, hasMipViews);
RecalculateHandleRegions();
}
/// <summary>
/// Consume the dirty flags for a given texture. The state is shared between views of the same layers and levels.
/// </summary>
/// <param name="texture">The texture being used</param>
/// <returns>True if a flag was dirty, false otherwise</returns>
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;
}
/// <summary>
/// Synchronize memory for a given texture.
/// If overlapping tracking handles are dirty, fully or partially synchronize the texture data.
/// </summary>
/// <param name="texture">The texture being used</param>
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();
}
}
});
}
/// <summary>
/// Synchronize part of the storage texture, represented by a given range of handles.
/// Only handles marked by the _loadNeeded array will be synchronized.
/// </summary>
/// <param name="baseHandle">The base index of the range of handles</param>
/// <param name="regionCount">The number of handles to synchronize</param>
private void SynchronizePartial(int baseHandle, int regionCount)
{
ReadOnlySpan<byte> 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<byte> data = fullData.Slice(offset, size);
data = Storage.ConvertToHostCompatibleFormat(data, info.BaseLevel, true);
Storage.SetData(data, info.BaseLayer, info.BaseLevel);
offsetIndex++;
}
}
}
}
}
/// <summary>
/// Signal that a texture in the group has been modified by the GPU.
/// </summary>
/// <param name="texture">The texture that has been modified</param>
/// <param name="registerAction">True if the flushing read action should be registered, false otherwise</param>
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);
}
}
});
}
/// <summary>
/// Signal that a texture in the group is actively bound, or has been unbound by the GPU.
/// </summary>
/// <param name="texture">The texture that has been modified</param>
/// <param name="bound">True if this texture is being bound, false if unbound</param>
/// <param name="registerAction">True if the flushing read action should be registered, false otherwise</param>
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);
}
}
});
}
/// <summary>
/// Register a read/write action to flush for a texture group.
/// </summary>
/// <param name="group">The group to register an action for</param>
public void RegisterAction(TextureGroupHandle group)
{
foreach (CpuRegionHandle handle in group.Handles)
{
handle.RegisterAction((address, size) => FlushAction(group, address, size));
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="hasLayerViews">True if the storage has layer views</param>
/// <param name="hasMipViews">True if the storage has mip views</param>
/// <returns>The input values after propagation</returns>
private (bool HasLayerViews, bool HasMipViews) PropagateGranularity(bool hasLayerViews, bool hasMipViews)
{
if (_is3D)
{
hasMipViews |= hasLayerViews;
}
else
{
hasLayerViews |= hasMipViews;
}
return (hasLayerViews, hasMipViews);
}
/// <summary>
/// Evaluate the range of tracking handles which a view texture overlaps with.
/// </summary>
/// <param name="texture">The texture to get handles for</param>
/// <param name="callback">
/// 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.
/// </param>
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);
}
/// <summary>
/// Evaluate the range of tracking handles which a view texture overlaps with,
/// using the view's position and slice/level counts.
/// </summary>
/// <param name="firstLayer">The first layer of the texture</param>
/// <param name="firstLevel">The first level of the texture</param>
/// <param name="slices">The slice count of the texture</param>
/// <param name="levels">The level count of the texture</param>
/// <param name="callback">
/// 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.
/// </param>
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);
}
}
}
/// <summary>
/// Get the range of offsets for a given mip level of a 3D texture.
/// </summary>
/// <param name="level">The level to return</param>
/// <returns>Start index and count of offsets for the given level</returns>
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);
}
/// <summary>
/// Get view information for a single tracking handle.
/// </summary>
/// <param name="handleIndex">The index of the handle</param>
/// <returns>The layers and levels that the handle covers, and its index in the offsets array</returns>
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);
}
/// <summary>
/// Gets the layer and level for a given view.
/// </summary>
/// <param name="index">The index of the view</param>
/// <returns>The layer and level of the specified view</returns>
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);
}
}
/// <summary>
/// Find the byte offset of a given texture relative to the storage.
/// </summary>
/// <param name="texture">The texture to locate</param>
/// <returns>The offset of the texture in bytes</returns>
public int FindOffset(Texture texture)
{
return _allOffsets[GetOffsetIndex(texture.FirstLayer, texture.FirstLevel)];
}
/// <summary>
/// Find the offset index of a given layer and level.
/// </summary>
/// <param name="layer">The view layer</param>
/// <param name="level">The view level</param>
/// <returns>The offset index of the given layer and level</returns>
public int GetOffsetIndex(int layer, int level)
{
if (_is3D)
{
return layer + Get3DLevelRange(level).Index;
}
else
{
return level + layer * _levels;
}
}
/// <summary>
/// The action to perform when a memory tracking handle is flipped to dirty.
/// This notifies overlapping textures that the memory needs to be synchronized.
/// </summary>
/// <param name="groupHandle">The handle that a dirty flag was set on</param>
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();
}
}
}
/// <summary>
/// Generate a CpuRegionHandle for a given address and size range in CPU VA.
/// </summary>
/// <param name="address">The start address of the tracked region</param>
/// <param name="size">The size of the tracked region</param>
/// <returns>A CpuRegionHandle covering the given range</returns>
private CpuRegionHandle GenerateHandle(ulong address, ulong size)
{
return _context.PhysicalMemory.BeginTracking(address, size);
}
/// <summary>
/// Generate a TextureGroupHandle covering a specified range of views.
/// </summary>
/// <param name="viewStart">The start view of the handle</param>
/// <param name="views">The number of views to cover</param>
/// <returns>A TextureGroupHandle covering the given views</returns>
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<CpuRegionHandle>();
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;
}
/// <summary>
/// Update the views in this texture group, rebuilding the memory tracking if required.
/// </summary>
/// <param name="views">The views list of the storage texture</param>
public void UpdateViews(List<Texture> 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();
}
}
/// <summary>
/// Inherit handle state from an old set of handles, such as modified and dirty flags.
/// </summary>
/// <param name="oldHandles">The set of handles to inherit state from</param>
/// <param name="handles">The set of handles inheriting the state</param>
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));
}
}
}
}
/// <summary>
/// Inherit state from another texture group.
/// </summary>
/// <param name="other">The texture group to inherit from</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="handles">The handles to replace the current handles with</param>
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];
}
/// <summary>
/// Recalculate handle regions for this texture group, and inherit existing state into the new handles.
/// </summary>
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<TextureGroupHandle>();
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);
}
/// <summary>
/// Ensure that there is a handle for each potential texture view. Required for copy dependencies to work.
/// </summary>
private void EnsureFullSubdivision()
{
if (!(_hasLayerViews && _hasMipViews))
{
_hasLayerViews = true;
_hasMipViews = true;
RecalculateHandleRegions();
}
}
/// <summary>
/// Create a copy dependency between this texture group, and a texture at a given layer/level offset.
/// </summary>
/// <param name="other">The view compatible texture to create a dependency to</param>
/// <param name="firstLayer">The base layer of the given texture relative to the storage</param>
/// <param name="firstLevel">The base level of the given texture relative to the storage</param>
/// <param name="copyTo">True if this texture is first copied to the given one, false for the opposite direction</param>
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);
}
}
}
/// <summary>
/// A flush has been requested on a tracked region. Find an appropriate view to flush.
/// </summary>
/// <param name="handle">The handle this flush action is for</param>
/// <param name="address">The address of the flushing memory access</param>
/// <param name="size">The size of the flushing memory access</param>
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;
}
/// <summary>
/// Dispose this texture group, disposing all related memory tracking handles.
/// </summary>
public void Dispose()
{
foreach (TextureGroupHandle group in _handles)
{
group.Dispose();
}
}
}
}

View File

@ -0,0 +1,327 @@
using Ryujinx.Cpu.Tracking;
using System;
using System.Collections.Generic;
using System.Linq;
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;
/// <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 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="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, CpuRegionHandle[] handles)
{
_group = group;
_firstLayer = firstLayer;
_firstLevel = firstLevel;
Offset = offset;
Size = (int)size;
Overlaps = new List<Texture>();
Dependencies = new List<TextureDependency>();
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>
/// Signal that this handle has been modified to any existing dependencies, and set the modified flag.
/// </summary>
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();
}
}
/// <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>
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));
}
/// <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)
{
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="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(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;
}
/// <summary>
/// Inherit modified flags and dependencies from another texture handle.
/// </summary>
/// <param name="old">The texture handle to inherit from</param>
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;
}
/// <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;
}
public void Dispose()
{
foreach (CpuRegionHandle handle in Handles)
{
handle.Dispose();
}
foreach (TextureDependency dependency in Dependencies.ToArray())
{
dependency.Other.Handle.RemoveDependency(dependency.Other);
}
}
}
}

View File

@ -232,6 +232,31 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The number of texture slices</returns>
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;
}
}
/// <summary>
/// Calculates the size information from the texture information.
/// </summary>

View File

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

View File

@ -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);
}
}
}

View File

@ -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<byte> data, int layer, int level)
{
throw new NotSupportedException();
}
public void SetStorage(BufferRange buffer)
{
if (buffer.Handle == _buffer &&

View File

@ -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++)
{

View File

@ -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<byte> 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);

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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<VirtualRegion> _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
/// <summary>
/// Consume the dirty flag for this handle, and reprotect so it can be set on the next write.
/// </summary>
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
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="action">Action to call on dirty</param>
public void RegisterDirtyEvent(Action action)
{
_onDirty += action;
}
/// <summary>
/// Add a child virtual region to this handle.
/// </summary>
@ -125,6 +144,13 @@ namespace Ryujinx.Memory.Tracking
/// </summary>
public void Dispose()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
_disposed = true;
lock (_tracking.TrackingLock)
{
foreach (VirtualRegion region in _regions)