mirror of
https://github.com/Ryujinx/Ryujinx.git
synced 2024-12-01 01:10:16 +00:00
Workaround for AMD and Intel view format bug (#1050)
* Workaround for Intel view format bug * Dispose of the intermmediate texture aswell * Apply workaround on AMD aswell
This commit is contained in:
parent
5c1757f7c2
commit
b18ef8e3a0
7 changed files with 222 additions and 119 deletions
|
@ -34,7 +34,7 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
switch (type)
|
||||
{
|
||||
case DebugType.DebugTypeError:
|
||||
Logger.PrintDebug(LogClass.Gpu, fullMessage);
|
||||
Logger.PrintError(LogClass.Gpu, fullMessage);
|
||||
break;
|
||||
case DebugType.DebugTypePerformance:
|
||||
Logger.PrintWarning(LogClass.Gpu, fullMessage);
|
||||
|
|
|
@ -10,9 +10,13 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
private FramebufferAttachment _lastDsAttachment;
|
||||
|
||||
private readonly TextureView[] _colors;
|
||||
|
||||
public Framebuffer()
|
||||
{
|
||||
Handle = GL.GenFramebuffer();
|
||||
|
||||
_colors = new TextureView[8];
|
||||
}
|
||||
|
||||
public void Bind()
|
||||
|
@ -22,11 +26,19 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
public void AttachColor(int index, TextureView color)
|
||||
{
|
||||
GL.FramebufferTexture(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.ColorAttachment0 + index,
|
||||
color?.Handle ?? 0,
|
||||
0);
|
||||
FramebufferAttachment attachment = FramebufferAttachment.ColorAttachment0 + index;
|
||||
|
||||
if (HwCapabilities.Vendor == HwCapabilities.GpuVendor.Amd ||
|
||||
HwCapabilities.Vendor == HwCapabilities.GpuVendor.Intel)
|
||||
{
|
||||
GL.FramebufferTexture(FramebufferTarget.Framebuffer, attachment, color?.GetIncompatibleFormatViewHandle() ?? 0, 0);
|
||||
|
||||
_colors[index] = color;
|
||||
}
|
||||
else
|
||||
{
|
||||
GL.FramebufferTexture(FramebufferTarget.Framebuffer, attachment, color?.Handle ?? 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachDepthStencil(TextureView depthStencil)
|
||||
|
@ -68,6 +80,21 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
}
|
||||
}
|
||||
|
||||
public void SignalModified()
|
||||
{
|
||||
if (HwCapabilities.Vendor == HwCapabilities.GpuVendor.Amd ||
|
||||
HwCapabilities.Vendor == HwCapabilities.GpuVendor.Intel)
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (_colors[i] != null)
|
||||
{
|
||||
_colors[i].SignalModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDrawBuffers(int colorsCount)
|
||||
{
|
||||
DrawBuffersEnum[] drawBuffers = new DrawBuffersEnum[colorsCount];
|
||||
|
|
|
@ -5,15 +5,25 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
{
|
||||
static class HwCapabilities
|
||||
{
|
||||
private static Lazy<bool> _supportsAstcCompression = new Lazy<bool>(() => HasExtension("GL_KHR_texture_compression_astc_ldr"));
|
||||
private static readonly Lazy<bool> _supportsAstcCompression = new Lazy<bool>(() => HasExtension("GL_KHR_texture_compression_astc_ldr"));
|
||||
|
||||
private static Lazy<int> _maximumComputeSharedMemorySize = new Lazy<int>(() => GetLimit(All.MaxComputeSharedMemorySize));
|
||||
private static Lazy<int> _storageBufferOffsetAlignment = new Lazy<int>(() => GetLimit(All.ShaderStorageBufferOffsetAlignment));
|
||||
private static readonly Lazy<int> _maximumComputeSharedMemorySize = new Lazy<int>(() => GetLimit(All.MaxComputeSharedMemorySize));
|
||||
private static readonly Lazy<int> _storageBufferOffsetAlignment = new Lazy<int>(() => GetLimit(All.ShaderStorageBufferOffsetAlignment));
|
||||
|
||||
private static Lazy<bool> _isNvidiaDriver = new Lazy<bool>(() => IsNvidiaDriver());
|
||||
public enum GpuVendor
|
||||
{
|
||||
Unknown,
|
||||
Amd,
|
||||
Intel,
|
||||
Nvidia
|
||||
}
|
||||
|
||||
private static readonly Lazy<GpuVendor> _gpuVendor = new Lazy<GpuVendor>(GetGpuVendor);
|
||||
|
||||
public static GpuVendor Vendor => _gpuVendor.Value;
|
||||
|
||||
public static bool SupportsAstcCompression => _supportsAstcCompression.Value;
|
||||
public static bool SupportsNonConstantTextureOffset => _isNvidiaDriver.Value;
|
||||
public static bool SupportsNonConstantTextureOffset => _gpuVendor.Value == GpuVendor.Nvidia;
|
||||
|
||||
public static int MaximumComputeSharedMemorySize => _maximumComputeSharedMemorySize.Value;
|
||||
public static int StorageBufferOffsetAlignment => _storageBufferOffsetAlignment.Value;
|
||||
|
@ -38,9 +48,26 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
return GL.GetInteger((GetPName)name);
|
||||
}
|
||||
|
||||
private static bool IsNvidiaDriver()
|
||||
private static GpuVendor GetGpuVendor()
|
||||
{
|
||||
return GL.GetString(StringName.Vendor).Equals("NVIDIA Corporation");
|
||||
string vendor = GL.GetString(StringName.Vendor).ToLower();
|
||||
|
||||
if (vendor == "nvidia corporation")
|
||||
{
|
||||
return GpuVendor.Nvidia;
|
||||
}
|
||||
else if (vendor == "intel")
|
||||
{
|
||||
return GpuVendor.Intel;
|
||||
}
|
||||
else if (vendor == "ati technologies inc." || vendor == "advanced micro devices, inc.")
|
||||
{
|
||||
return GpuVendor.Amd;
|
||||
}
|
||||
else
|
||||
{
|
||||
return GpuVendor.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,6 +60,8 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
GL.ClearBuffer(ClearBuffer.Color, index, colors);
|
||||
|
||||
RestoreComponentMask(index);
|
||||
|
||||
_framebuffer.SignalModified();
|
||||
}
|
||||
|
||||
public void ClearRenderTargetDepthStencil(float depthValue, bool depthMask, int stencilValue, int stencilMask)
|
||||
|
@ -102,6 +104,8 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
{
|
||||
GL.DepthMask(_depthMask);
|
||||
}
|
||||
|
||||
_framebuffer.SignalModified();
|
||||
}
|
||||
|
||||
public void DispatchCompute(int groupsX, int groupsY, int groupsZ)
|
||||
|
@ -141,6 +145,8 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
{
|
||||
DrawImpl(vertexCount, instanceCount, firstVertex, firstInstance);
|
||||
}
|
||||
|
||||
_framebuffer.SignalModified();
|
||||
}
|
||||
|
||||
private void DrawQuadsImpl(
|
||||
|
@ -285,6 +291,8 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
firstVertex,
|
||||
firstInstance);
|
||||
}
|
||||
|
||||
_framebuffer.SignalModified();
|
||||
}
|
||||
|
||||
private void DrawQuadsIndexedImpl(
|
||||
|
|
|
@ -7,22 +7,30 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
{
|
||||
static class TextureCopyUnscaled
|
||||
{
|
||||
public static void Copy(TextureView src, TextureView dst, int dstLayer, int dstLevel)
|
||||
public static void Copy(
|
||||
TextureCreateInfo srcInfo,
|
||||
TextureCreateInfo dstInfo,
|
||||
int srcHandle,
|
||||
int dstHandle,
|
||||
int srcLayer,
|
||||
int dstLayer,
|
||||
int srcLevel,
|
||||
int dstLevel)
|
||||
{
|
||||
int srcWidth = src.Width;
|
||||
int srcHeight = src.Height;
|
||||
int srcDepth = src.DepthOrLayers;
|
||||
int srcLevels = src.Levels;
|
||||
int srcWidth = srcInfo.Width;
|
||||
int srcHeight = srcInfo.Height;
|
||||
int srcDepth = srcInfo.GetDepthOrLayers();
|
||||
int srcLevels = srcInfo.Levels;
|
||||
|
||||
int dstWidth = dst.Width;
|
||||
int dstHeight = dst.Height;
|
||||
int dstDepth = dst.DepthOrLayers;
|
||||
int dstLevels = dst.Levels;
|
||||
int dstWidth = dstInfo.Width;
|
||||
int dstHeight = dstInfo.Height;
|
||||
int dstDepth = dstInfo.GetDepthOrLayers();
|
||||
int dstLevels = dstInfo.Levels;
|
||||
|
||||
dstWidth = Math.Max(1, dstWidth >> dstLevel);
|
||||
dstHeight = Math.Max(1, dstHeight >> dstLevel);
|
||||
|
||||
if (dst.Target == Target.Texture3D)
|
||||
if (dstInfo.Target == Target.Texture3D)
|
||||
{
|
||||
dstDepth = Math.Max(1, dstDepth >> dstLevel);
|
||||
}
|
||||
|
@ -31,15 +39,15 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
// the non-compressed texture will have the size of the texture
|
||||
// in blocks (not in texels), so we must adjust that size to
|
||||
// match the size in texels of the compressed texture.
|
||||
if (!src.IsCompressed && dst.IsCompressed)
|
||||
if (!srcInfo.IsCompressed && dstInfo.IsCompressed)
|
||||
{
|
||||
dstWidth = BitUtils.DivRoundUp(dstWidth, dst.BlockWidth);
|
||||
dstHeight = BitUtils.DivRoundUp(dstHeight, dst.BlockHeight);
|
||||
dstWidth = BitUtils.DivRoundUp(dstWidth, dstInfo.BlockWidth);
|
||||
dstHeight = BitUtils.DivRoundUp(dstHeight, dstInfo.BlockHeight);
|
||||
}
|
||||
else if (src.IsCompressed && !dst.IsCompressed)
|
||||
else if (srcInfo.IsCompressed && !dstInfo.IsCompressed)
|
||||
{
|
||||
dstWidth *= dst.BlockWidth;
|
||||
dstHeight *= dst.BlockHeight;
|
||||
dstWidth *= dstInfo.BlockWidth;
|
||||
dstHeight *= dstInfo.BlockHeight;
|
||||
}
|
||||
|
||||
int width = Math.Min(srcWidth, dstWidth);
|
||||
|
@ -50,20 +58,20 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
for (int level = 0; level < levels; level++)
|
||||
{
|
||||
// Stop copy if we are already out of the levels range.
|
||||
if (level >= src.Levels || dstLevel + level >= dst.Levels)
|
||||
if (level >= srcInfo.Levels || dstLevel + level >= dstInfo.Levels)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
GL.CopyImageSubData(
|
||||
src.Handle,
|
||||
src.Target.ConvertToImageTarget(),
|
||||
level,
|
||||
srcHandle,
|
||||
srcInfo.Target.ConvertToImageTarget(),
|
||||
srcLevel + level,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
dst.Handle,
|
||||
dst.Target.ConvertToImageTarget(),
|
||||
srcLayer,
|
||||
dstHandle,
|
||||
dstInfo.Target.ConvertToImageTarget(),
|
||||
dstLevel + level,
|
||||
0,
|
||||
0,
|
||||
|
@ -75,7 +83,7 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
width = Math.Max(1, width >> 1);
|
||||
height = Math.Max(1, height >> 1);
|
||||
|
||||
if (src.Target == Target.Texture3D)
|
||||
if (srcInfo.Target == Target.Texture3D)
|
||||
{
|
||||
depth = Math.Max(1, depth >> 1);
|
||||
}
|
||||
|
|
|
@ -8,18 +8,16 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
{
|
||||
public int Handle { get; private set; }
|
||||
|
||||
public TextureCreateInfo Info { get; }
|
||||
|
||||
private readonly Renderer _renderer;
|
||||
|
||||
private readonly TextureCreateInfo _info;
|
||||
|
||||
public Target Target => _info.Target;
|
||||
|
||||
private int _viewsCount;
|
||||
|
||||
public TextureStorage(Renderer renderer, TextureCreateInfo info)
|
||||
{
|
||||
_renderer = renderer;
|
||||
_info = info;
|
||||
Info = info;
|
||||
|
||||
Handle = GL.GenTexture();
|
||||
|
||||
|
@ -28,13 +26,13 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
private void CreateImmutableStorage()
|
||||
{
|
||||
TextureTarget target = _info.Target.Convert();
|
||||
TextureTarget target = Info.Target.Convert();
|
||||
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
|
||||
GL.BindTexture(target, Handle);
|
||||
|
||||
FormatInfo format = FormatTable.GetFormatInfo(_info.Format);
|
||||
FormatInfo format = FormatTable.GetFormatInfo(Info.Format);
|
||||
|
||||
SizedInternalFormat internalFormat;
|
||||
|
||||
|
@ -47,92 +45,92 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
internalFormat = (SizedInternalFormat)format.PixelInternalFormat;
|
||||
}
|
||||
|
||||
switch (_info.Target)
|
||||
switch (Info.Target)
|
||||
{
|
||||
case Target.Texture1D:
|
||||
GL.TexStorage1D(
|
||||
TextureTarget1d.Texture1D,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width);
|
||||
Info.Width);
|
||||
break;
|
||||
|
||||
case Target.Texture1DArray:
|
||||
GL.TexStorage2D(
|
||||
TextureTarget2d.Texture1DArray,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height);
|
||||
Info.Width,
|
||||
Info.Height);
|
||||
break;
|
||||
|
||||
case Target.Texture2D:
|
||||
GL.TexStorage2D(
|
||||
TextureTarget2d.Texture2D,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height);
|
||||
Info.Width,
|
||||
Info.Height);
|
||||
break;
|
||||
|
||||
case Target.Texture2DArray:
|
||||
GL.TexStorage3D(
|
||||
TextureTarget3d.Texture2DArray,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height,
|
||||
_info.Depth);
|
||||
Info.Width,
|
||||
Info.Height,
|
||||
Info.Depth);
|
||||
break;
|
||||
|
||||
case Target.Texture2DMultisample:
|
||||
GL.TexStorage2DMultisample(
|
||||
TextureTargetMultisample2d.Texture2DMultisample,
|
||||
_info.Samples,
|
||||
Info.Samples,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height,
|
||||
Info.Width,
|
||||
Info.Height,
|
||||
true);
|
||||
break;
|
||||
|
||||
case Target.Texture2DMultisampleArray:
|
||||
GL.TexStorage3DMultisample(
|
||||
TextureTargetMultisample3d.Texture2DMultisampleArray,
|
||||
_info.Samples,
|
||||
Info.Samples,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height,
|
||||
_info.Depth,
|
||||
Info.Width,
|
||||
Info.Height,
|
||||
Info.Depth,
|
||||
true);
|
||||
break;
|
||||
|
||||
case Target.Texture3D:
|
||||
GL.TexStorage3D(
|
||||
TextureTarget3d.Texture3D,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height,
|
||||
_info.Depth);
|
||||
Info.Width,
|
||||
Info.Height,
|
||||
Info.Depth);
|
||||
break;
|
||||
|
||||
case Target.Cubemap:
|
||||
GL.TexStorage2D(
|
||||
TextureTarget2d.TextureCubeMap,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height);
|
||||
Info.Width,
|
||||
Info.Height);
|
||||
break;
|
||||
|
||||
case Target.CubemapArray:
|
||||
GL.TexStorage3D(
|
||||
(TextureTarget3d)All.TextureCubeMapArray,
|
||||
_info.Levels,
|
||||
Info.Levels,
|
||||
internalFormat,
|
||||
_info.Width,
|
||||
_info.Height,
|
||||
_info.Depth);
|
||||
Info.Width,
|
||||
Info.Height,
|
||||
Info.Depth);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -143,7 +141,7 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
public ITexture CreateDefaultView()
|
||||
{
|
||||
return CreateView(_info, 0, 0);
|
||||
return CreateView(Info, 0, 0);
|
||||
}
|
||||
|
||||
public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel)
|
||||
|
|
|
@ -14,24 +14,19 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
private TextureView _emulatedViewParent;
|
||||
|
||||
private TextureView _incompatibleFormatView;
|
||||
|
||||
private readonly TextureCreateInfo _info;
|
||||
|
||||
private int _firstLayer;
|
||||
private int _firstLevel;
|
||||
public int FirstLayer { get; private set; }
|
||||
public int FirstLevel { get; private set; }
|
||||
|
||||
public int Width => _info.Width;
|
||||
public int Height => _info.Height;
|
||||
public int DepthOrLayers => _info.GetDepthOrLayers();
|
||||
public int Levels => _info.Levels;
|
||||
|
||||
public Target Target => _info.Target;
|
||||
public Format Format => _info.Format;
|
||||
|
||||
public int BlockWidth => _info.BlockWidth;
|
||||
public int BlockHeight => _info.BlockHeight;
|
||||
|
||||
public bool IsCompressed => _info.IsCompressed;
|
||||
|
||||
public TextureView(
|
||||
Renderer renderer,
|
||||
TextureStorage parent,
|
||||
|
@ -43,8 +38,8 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
_parent = parent;
|
||||
_info = info;
|
||||
|
||||
_firstLayer = firstLayer;
|
||||
_firstLevel = firstLevel;
|
||||
FirstLayer = firstLayer;
|
||||
FirstLevel = firstLevel;
|
||||
|
||||
Handle = GL.GenTexture();
|
||||
|
||||
|
@ -73,9 +68,9 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
target,
|
||||
_parent.Handle,
|
||||
pixelInternalFormat,
|
||||
_firstLevel,
|
||||
FirstLevel,
|
||||
_info.Levels,
|
||||
_firstLayer,
|
||||
FirstLayer,
|
||||
_info.GetLayers());
|
||||
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
|
@ -107,8 +102,8 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
{
|
||||
if (_info.IsCompressed == info.IsCompressed)
|
||||
{
|
||||
firstLayer += _firstLayer;
|
||||
firstLevel += _firstLevel;
|
||||
firstLayer += FirstLayer;
|
||||
firstLevel += FirstLevel;
|
||||
|
||||
return _parent.CreateView(info, firstLayer, firstLevel);
|
||||
}
|
||||
|
@ -123,26 +118,59 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
emulatedView._emulatedViewParent = this;
|
||||
|
||||
emulatedView._firstLayer = firstLayer;
|
||||
emulatedView._firstLevel = firstLevel;
|
||||
emulatedView.FirstLayer = firstLayer;
|
||||
emulatedView.FirstLevel = firstLevel;
|
||||
|
||||
return emulatedView;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetIncompatibleFormatViewHandle()
|
||||
{
|
||||
// AMD and Intel has a bug where the view format is always ignored,
|
||||
// it uses the parent format instead.
|
||||
// As workaround we create a new texture with the correct
|
||||
// format, and then do a copy after the draw.
|
||||
if (_parent.Info.Format != Format)
|
||||
{
|
||||
if (_incompatibleFormatView == null)
|
||||
{
|
||||
_incompatibleFormatView = (TextureView)_renderer.CreateTexture(_info);
|
||||
}
|
||||
|
||||
TextureCopyUnscaled.Copy(_parent.Info, _incompatibleFormatView._info, _parent.Handle, _incompatibleFormatView.Handle, FirstLayer, 0, FirstLevel, 0);
|
||||
|
||||
return _incompatibleFormatView.Handle;
|
||||
}
|
||||
|
||||
return Handle;
|
||||
}
|
||||
|
||||
public void SignalModified()
|
||||
{
|
||||
if (_incompatibleFormatView != null)
|
||||
{
|
||||
TextureCopyUnscaled.Copy(_incompatibleFormatView._info, _parent.Info, _incompatibleFormatView.Handle, _parent.Handle, 0, FirstLayer, 0, FirstLevel);
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(ITexture destination, int firstLayer, int firstLevel)
|
||||
{
|
||||
TextureView destinationView = (TextureView)destination;
|
||||
|
||||
TextureCopyUnscaled.Copy(this, destinationView, firstLayer, firstLevel);
|
||||
TextureCopyUnscaled.Copy(_info, destinationView._info, Handle, destinationView.Handle, 0, firstLayer, 0, firstLevel);
|
||||
|
||||
if (destinationView._emulatedViewParent != null)
|
||||
{
|
||||
TextureCopyUnscaled.Copy(
|
||||
this,
|
||||
destinationView._emulatedViewParent,
|
||||
destinationView._firstLayer,
|
||||
destinationView._firstLevel);
|
||||
_info,
|
||||
destinationView._emulatedViewParent._info,
|
||||
Handle,
|
||||
destinationView._emulatedViewParent.Handle,
|
||||
0,
|
||||
destinationView.FirstLayer,
|
||||
0,
|
||||
destinationView.FirstLevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,6 +433,13 @@ namespace Ryujinx.Graphics.OpenGL
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_incompatibleFormatView != null)
|
||||
{
|
||||
_incompatibleFormatView.Dispose();
|
||||
|
||||
_incompatibleFormatView = null;
|
||||
}
|
||||
|
||||
if (Handle != 0)
|
||||
{
|
||||
GL.DeleteTexture(Handle);
|
||||
|
|
Loading…
Reference in a new issue