Vulkan: Fix some issues with CacheByRange (#3743)
* Fix some issues with CacheByRange - Cache now clears under more circumstances, the most important being the fast path write. - Cache supports partial clear which should help when more buffers join. - Fixed an issue with I8->I16 conversion where it wouldn't register the buffer for use on dispose. Should hopefully fix issues with https://github.com/Ryujinx/Ryujinx-Games-List/issues/4010 and maybe others. * Fix collection modified exception * Fix accidental use of parameterless constructor * Replay DynamicState when restoring from helper shader
This commit is contained in:
parent
599d485bff
commit
1ca0517c99
6 changed files with 124 additions and 25 deletions
|
@ -109,12 +109,34 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
if (isWrite)
|
if (isWrite)
|
||||||
{
|
{
|
||||||
_cachedConvertedBuffers.Clear();
|
SignalWrite(0, Size);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buffer;
|
return _buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Auto<DisposableBuffer> GetBuffer(CommandBuffer commandBuffer, int offset, int size, bool isWrite = false)
|
||||||
|
{
|
||||||
|
if (isWrite)
|
||||||
|
{
|
||||||
|
SignalWrite(offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SignalWrite(int offset, int size)
|
||||||
|
{
|
||||||
|
if (offset == 0 && size == Size)
|
||||||
|
{
|
||||||
|
_cachedConvertedBuffers.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cachedConvertedBuffers.ClearRange(offset, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public BufferHandle GetHandle()
|
public BufferHandle GetHandle()
|
||||||
{
|
{
|
||||||
var handle = _bufferHandle;
|
var handle = _bufferHandle;
|
||||||
|
@ -183,6 +205,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
data.Slice(0, dataSize).CopyTo(new Span<byte>((void*)(_map + offset), dataSize));
|
data.Slice(0, dataSize).CopyTo(new Span<byte>((void*)(_map + offset), dataSize));
|
||||||
|
|
||||||
|
SignalWrite(offset, dataSize);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,7 +264,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
endRenderPass?.Invoke();
|
endRenderPass?.Invoke();
|
||||||
|
|
||||||
var dstBuffer = GetBuffer(cbs.CommandBuffer, true).Get(cbs, dstOffset, data.Length).Value;
|
var dstBuffer = GetBuffer(cbs.CommandBuffer, dstOffset, data.Length, true).Get(cbs, dstOffset, data.Length).Value;
|
||||||
|
|
||||||
InsertBufferBarrier(
|
InsertBufferBarrier(
|
||||||
_gd,
|
_gd,
|
||||||
|
@ -364,7 +388,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
public Auto<DisposableBuffer> GetBufferI8ToI16(CommandBufferScoped cbs, int offset, int size)
|
public Auto<DisposableBuffer> GetBufferI8ToI16(CommandBufferScoped cbs, int offset, int size)
|
||||||
{
|
{
|
||||||
var key = new I8ToI16CacheKey();
|
var key = new I8ToI16CacheKey(_gd);
|
||||||
|
|
||||||
if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder))
|
if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder))
|
||||||
{
|
{
|
||||||
|
@ -373,6 +397,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
_gd.PipelineInternal.EndRenderPass();
|
_gd.PipelineInternal.EndRenderPass();
|
||||||
_gd.HelperShader.ConvertI8ToI16(_gd, cbs, this, holder, offset, size);
|
_gd.HelperShader.ConvertI8ToI16(_gd, cbs, this, holder, offset, size);
|
||||||
|
|
||||||
|
key.SetBuffer(holder.GetBuffer());
|
||||||
|
|
||||||
_cachedConvertedBuffers.Add(offset, size, key, holder);
|
_cachedConvertedBuffers.Add(offset, size, key, holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,6 +443,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
_gd.PipelineInternal.EndRenderPass();
|
_gd.PipelineInternal.EndRenderPass();
|
||||||
_gd.HelperShader.ConvertIndexBuffer(_gd, cbs, this, holder, pattern, indexSize, offset, indexCount);
|
_gd.HelperShader.ConvertIndexBuffer(_gd, cbs, this, holder, pattern, indexSize, offset, indexCount);
|
||||||
|
|
||||||
|
key.SetBuffer(holder.GetBuffer());
|
||||||
|
|
||||||
_cachedConvertedBuffers.Add(offset, size, key, holder);
|
_cachedConvertedBuffers.Add(offset, size, key, holder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,16 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Auto<DisposableBuffer> GetBuffer(CommandBuffer commandBuffer, BufferHandle handle, int offset, int size, bool isWrite)
|
||||||
|
{
|
||||||
|
if (TryGetBuffer(handle, out var holder))
|
||||||
|
{
|
||||||
|
return holder.GetBuffer(commandBuffer, offset, size, isWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public Auto<DisposableBuffer> GetBufferI8ToI16(CommandBufferScoped cbs, BufferHandle handle, int offset, int size)
|
public Auto<DisposableBuffer> GetBufferI8ToI16(CommandBufferScoped cbs, BufferHandle handle, int offset, int size)
|
||||||
{
|
{
|
||||||
if (TryGetBuffer(handle, out var holder))
|
if (TryGetBuffer(handle, out var holder))
|
||||||
|
|
|
@ -25,6 +25,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
return other is I8ToI16CacheKey;
|
return other is I8ToI16CacheKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetBuffer(Auto<DisposableBuffer> buffer)
|
||||||
|
{
|
||||||
|
_buffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_gd.PipelineInternal.DirtyIndexBuffer(_buffer);
|
_gd.PipelineInternal.DirtyIndexBuffer(_buffer);
|
||||||
|
@ -160,6 +165,44 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearRange(int offset, int size)
|
||||||
|
{
|
||||||
|
if (_ranges != null && _ranges.Count > 0)
|
||||||
|
{
|
||||||
|
int end = offset + size;
|
||||||
|
|
||||||
|
List<ulong> toRemove = null;
|
||||||
|
|
||||||
|
foreach (KeyValuePair<ulong, List<Entry>> range in _ranges)
|
||||||
|
{
|
||||||
|
(int rOffset, int rSize) = UnpackRange(range.Key);
|
||||||
|
|
||||||
|
int rEnd = rOffset + rSize;
|
||||||
|
|
||||||
|
if (rEnd > offset && rOffset < end)
|
||||||
|
{
|
||||||
|
List<Entry> entries = range.Value;
|
||||||
|
|
||||||
|
foreach (Entry entry in entries)
|
||||||
|
{
|
||||||
|
entry.Key.Dispose();
|
||||||
|
entry.Value.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
(toRemove ??= new List<ulong>()).Add(range.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toRemove != null)
|
||||||
|
{
|
||||||
|
foreach (ulong range in toRemove)
|
||||||
|
{
|
||||||
|
_ranges.Remove(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<Entry> GetEntries(int offset, int size)
|
private List<Entry> GetEntries(int offset, int size)
|
||||||
{
|
{
|
||||||
if (_ranges == null)
|
if (_ranges == null)
|
||||||
|
@ -184,6 +227,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
return (uint)offset | ((ulong)size << 32);
|
return (uint)offset | ((ulong)size << 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (int offset, int size) UnpackRange(ulong range)
|
||||||
|
{
|
||||||
|
return ((int)range, (int)(range >> 32));
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
|
|
|
@ -21,7 +21,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
protected readonly AutoFlushCounter AutoFlush;
|
protected readonly AutoFlushCounter AutoFlush;
|
||||||
|
|
||||||
private PipelineDynamicState _dynamicState;
|
protected PipelineDynamicState DynamicState;
|
||||||
private PipelineState _newState;
|
private PipelineState _newState;
|
||||||
private bool _stateDirty;
|
private bool _stateDirty;
|
||||||
private GAL.PrimitiveTopology _topology;
|
private GAL.PrimitiveTopology _topology;
|
||||||
|
@ -150,7 +150,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
EndRenderPass();
|
EndRenderPass();
|
||||||
|
|
||||||
var dst = Gd.BufferManager.GetBuffer(CommandBuffer, destination, true).Get(Cbs, offset, size).Value;
|
var dst = Gd.BufferManager.GetBuffer(CommandBuffer, destination, offset, size, true).Get(Cbs, offset, size).Value;
|
||||||
|
|
||||||
BufferHolder.InsertBufferBarrier(
|
BufferHolder.InsertBufferBarrier(
|
||||||
Gd,
|
Gd,
|
||||||
|
@ -238,8 +238,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
EndRenderPass();
|
EndRenderPass();
|
||||||
|
|
||||||
var src = Gd.BufferManager.GetBuffer(CommandBuffer, source, false);
|
var src = Gd.BufferManager.GetBuffer(CommandBuffer, source, srcOffset, size, false);
|
||||||
var dst = Gd.BufferManager.GetBuffer(CommandBuffer, destination, true);
|
var dst = Gd.BufferManager.GetBuffer(CommandBuffer, destination, dstOffset, size, true);
|
||||||
|
|
||||||
BufferHolder.Copy(Gd, Cbs, src, dst, srcOffset, dstOffset, size);
|
BufferHolder.Copy(Gd, Cbs, src, dst, srcOffset, dstOffset, size);
|
||||||
}
|
}
|
||||||
|
@ -388,7 +388,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
var oldDepthTestEnable = _newState.DepthTestEnable;
|
var oldDepthTestEnable = _newState.DepthTestEnable;
|
||||||
var oldDepthWriteEnable = _newState.DepthWriteEnable;
|
var oldDepthWriteEnable = _newState.DepthWriteEnable;
|
||||||
var oldTopology = _newState.Topology;
|
var oldTopology = _newState.Topology;
|
||||||
var oldViewports = _dynamicState.Viewports;
|
var oldViewports = DynamicState.Viewports;
|
||||||
var oldViewportsCount = _newState.ViewportsCount;
|
var oldViewportsCount = _newState.ViewportsCount;
|
||||||
|
|
||||||
_newState.CullMode = CullModeFlags.CullModeNone;
|
_newState.CullMode = CullModeFlags.CullModeNone;
|
||||||
|
@ -411,9 +411,9 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
_newState.DepthWriteEnable = oldDepthWriteEnable;
|
_newState.DepthWriteEnable = oldDepthWriteEnable;
|
||||||
_newState.Topology = oldTopology;
|
_newState.Topology = oldTopology;
|
||||||
|
|
||||||
_dynamicState.Viewports = oldViewports;
|
DynamicState.Viewports = oldViewports;
|
||||||
_dynamicState.ViewportsCount = (int)oldViewportsCount;
|
DynamicState.ViewportsCount = (int)oldViewportsCount;
|
||||||
_dynamicState.SetViewportsDirty();
|
DynamicState.SetViewportsDirty();
|
||||||
|
|
||||||
_newState.ViewportsCount = oldViewportsCount;
|
_newState.ViewportsCount = oldViewportsCount;
|
||||||
SignalStateChange();
|
SignalStateChange();
|
||||||
|
@ -448,8 +448,13 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
ResumeTransformFeedbackInternal();
|
ResumeTransformFeedbackInternal();
|
||||||
DrawCount++;
|
DrawCount++;
|
||||||
|
|
||||||
var buffer = Gd.BufferManager.GetBuffer(CommandBuffer, indirectBuffer.Handle, true).Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value;
|
var buffer = Gd.BufferManager
|
||||||
var countBuffer = Gd.BufferManager.GetBuffer(CommandBuffer, parameterBuffer.Handle, true).Get(Cbs, parameterBuffer.Offset, parameterBuffer.Size).Value;
|
.GetBuffer(CommandBuffer, indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, true)
|
||||||
|
.Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value;
|
||||||
|
|
||||||
|
var countBuffer = Gd.BufferManager
|
||||||
|
.GetBuffer(CommandBuffer, parameterBuffer.Handle, parameterBuffer.Offset, parameterBuffer.Size, true)
|
||||||
|
.Get(Cbs, parameterBuffer.Offset, parameterBuffer.Size).Value;
|
||||||
|
|
||||||
Gd.DrawIndirectCountApi.CmdDrawIndirectCount(
|
Gd.DrawIndirectCountApi.CmdDrawIndirectCount(
|
||||||
CommandBuffer,
|
CommandBuffer,
|
||||||
|
@ -478,8 +483,13 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
ResumeTransformFeedbackInternal();
|
ResumeTransformFeedbackInternal();
|
||||||
DrawCount++;
|
DrawCount++;
|
||||||
|
|
||||||
var buffer = Gd.BufferManager.GetBuffer(CommandBuffer, indirectBuffer.Handle, true).Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value;
|
var buffer = Gd.BufferManager
|
||||||
var countBuffer = Gd.BufferManager.GetBuffer(CommandBuffer, parameterBuffer.Handle, true).Get(Cbs, parameterBuffer.Offset, parameterBuffer.Size).Value;
|
.GetBuffer(CommandBuffer, indirectBuffer.Handle, parameterBuffer.Offset, parameterBuffer.Size, true)
|
||||||
|
.Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value;
|
||||||
|
|
||||||
|
var countBuffer = Gd.BufferManager
|
||||||
|
.GetBuffer(CommandBuffer, parameterBuffer.Handle, parameterBuffer.Offset, parameterBuffer.Size, true)
|
||||||
|
.Get(Cbs, parameterBuffer.Offset, parameterBuffer.Size).Value;
|
||||||
|
|
||||||
Gd.DrawIndirectCountApi.CmdDrawIndexedIndirectCount(
|
Gd.DrawIndirectCountApi.CmdDrawIndexedIndirectCount(
|
||||||
CommandBuffer,
|
CommandBuffer,
|
||||||
|
@ -535,7 +545,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp)
|
public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp)
|
||||||
{
|
{
|
||||||
_dynamicState.SetDepthBias(factor, units, clamp);
|
DynamicState.SetDepthBias(factor, units, clamp);
|
||||||
|
|
||||||
_newState.DepthBiasEnable = enables != 0;
|
_newState.DepthBiasEnable = enables != 0;
|
||||||
SignalStateChange();
|
SignalStateChange();
|
||||||
|
@ -753,10 +763,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
var offset = new Offset2D(region.X, region.Y);
|
var offset = new Offset2D(region.X, region.Y);
|
||||||
var extent = new Extent2D((uint)region.Width, (uint)region.Height);
|
var extent = new Extent2D((uint)region.Width, (uint)region.Height);
|
||||||
|
|
||||||
_dynamicState.SetScissor(i, new Rect2D(offset, extent));
|
DynamicState.SetScissor(i, new Rect2D(offset, extent));
|
||||||
}
|
}
|
||||||
|
|
||||||
_dynamicState.ScissorsCount = count;
|
DynamicState.ScissorsCount = count;
|
||||||
|
|
||||||
_newState.ScissorsCount = (uint)count;
|
_newState.ScissorsCount = (uint)count;
|
||||||
SignalStateChange();
|
SignalStateChange();
|
||||||
|
@ -764,7 +774,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
public void SetStencilTest(StencilTestDescriptor stencilTest)
|
public void SetStencilTest(StencilTestDescriptor stencilTest)
|
||||||
{
|
{
|
||||||
_dynamicState.SetStencilMasks(
|
DynamicState.SetStencilMasks(
|
||||||
(uint)stencilTest.BackFuncMask,
|
(uint)stencilTest.BackFuncMask,
|
||||||
(uint)stencilTest.BackMask,
|
(uint)stencilTest.BackMask,
|
||||||
(uint)stencilTest.BackFuncRef,
|
(uint)stencilTest.BackFuncRef,
|
||||||
|
@ -813,7 +823,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
if (range.Handle != BufferHandle.Null)
|
if (range.Handle != BufferHandle.Null)
|
||||||
{
|
{
|
||||||
_transformFeedbackBuffers[i] = new BufferState(Gd.BufferManager.GetBuffer(CommandBuffer, range.Handle, true), range.Offset, range.Size);
|
_transformFeedbackBuffers[i] =
|
||||||
|
new BufferState(Gd.BufferManager.GetBuffer(CommandBuffer, range.Handle, range.Offset, range.Size, true), range.Offset, range.Size);
|
||||||
_transformFeedbackBuffers[i].BindTransformFeedbackBuffer(Gd, Cbs, (uint)i);
|
_transformFeedbackBuffers[i].BindTransformFeedbackBuffer(Gd, Cbs, (uint)i);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -975,7 +986,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
var viewport = viewports[i];
|
var viewport = viewports[i];
|
||||||
|
|
||||||
_dynamicState.SetViewport(i, new Silk.NET.Vulkan.Viewport(
|
DynamicState.SetViewport(i, new Silk.NET.Vulkan.Viewport(
|
||||||
viewport.Region.X,
|
viewport.Region.X,
|
||||||
viewport.Region.Y,
|
viewport.Region.Y,
|
||||||
viewport.Region.Width == 0f ? 1f : viewport.Region.Width,
|
viewport.Region.Width == 0f ? 1f : viewport.Region.Width,
|
||||||
|
@ -984,7 +995,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
Clamp(viewport.DepthFar)));
|
Clamp(viewport.DepthFar)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_dynamicState.ViewportsCount = count;
|
DynamicState.ViewportsCount = count;
|
||||||
|
|
||||||
float disableTransformF = disableTransform ? 1.0f : 0.0f;
|
float disableTransformF = disableTransform ? 1.0f : 0.0f;
|
||||||
if (SupportBufferUpdater.Data.ViewportInverse.W != disableTransformF || disableTransform)
|
if (SupportBufferUpdater.Data.ViewportInverse.W != disableTransformF || disableTransform)
|
||||||
|
@ -1063,7 +1074,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
_vertexBuffersDirty = ulong.MaxValue >> (64 - _vertexBuffers.Length);
|
_vertexBuffersDirty = ulong.MaxValue >> (64 - _vertexBuffers.Length);
|
||||||
|
|
||||||
_descriptorSetUpdater.SignalCommandBufferChange();
|
_descriptorSetUpdater.SignalCommandBufferChange();
|
||||||
_dynamicState.ForceAllDirty();
|
DynamicState.ForceAllDirty();
|
||||||
_currentPipelineHandle = 0;
|
_currentPipelineHandle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1201,7 +1212,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
private void RecreatePipelineIfNeeded(PipelineBindPoint pbp)
|
private void RecreatePipelineIfNeeded(PipelineBindPoint pbp)
|
||||||
{
|
{
|
||||||
_dynamicState.ReplayIfDirty(Gd.Api, CommandBuffer);
|
DynamicState.ReplayIfDirty(Gd.Api, CommandBuffer);
|
||||||
|
|
||||||
// Commit changes to the support buffer before drawing.
|
// Commit changes to the support buffer before drawing.
|
||||||
SupportBufferUpdater.Commit();
|
SupportBufferUpdater.Commit();
|
||||||
|
|
|
@ -204,6 +204,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalCommandBufferChange();
|
SignalCommandBufferChange();
|
||||||
|
|
||||||
|
DynamicState.ReplayIfDirty(Gd.Api, CommandBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FlushCommandsImpl()
|
public void FlushCommandsImpl()
|
||||||
|
|
|
@ -87,7 +87,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
private void PushDataImpl(CommandBufferScoped cbs, BufferHolder dst, int dstOffset, ReadOnlySpan<byte> data)
|
private void PushDataImpl(CommandBufferScoped cbs, BufferHolder dst, int dstOffset, ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
var srcBuffer = _buffer.GetBuffer();
|
var srcBuffer = _buffer.GetBuffer();
|
||||||
var dstBuffer = dst.GetBuffer();
|
var dstBuffer = dst.GetBuffer(cbs.CommandBuffer, dstOffset, data.Length, true);
|
||||||
|
|
||||||
int offset = _freeOffset;
|
int offset = _freeOffset;
|
||||||
int capacity = BufferSize - offset;
|
int capacity = BufferSize - offset;
|
||||||
|
|
Loading…
Reference in a new issue