forked from Mirror/Ryujinx
Implement S8D24 texture format and tweak depth range detection (#2458)
This commit is contained in:
parent
79becc4b78
commit
e5ad1dfa48
10 changed files with 196 additions and 30 deletions
|
@ -52,7 +52,7 @@ namespace Ryujinx.Graphics.GAL
|
||||||
R32G32B32A32Sint,
|
R32G32B32A32Sint,
|
||||||
S8Uint,
|
S8Uint,
|
||||||
D16Unorm,
|
D16Unorm,
|
||||||
D24X8Unorm,
|
S8UintD24Unorm,
|
||||||
D32Float,
|
D32Float,
|
||||||
D24UnormS8Uint,
|
D24UnormS8Uint,
|
||||||
D32FloatS8Uint,
|
D32FloatS8Uint,
|
||||||
|
@ -266,7 +266,7 @@ namespace Ryujinx.Graphics.GAL
|
||||||
{
|
{
|
||||||
case Format.D16Unorm:
|
case Format.D16Unorm:
|
||||||
case Format.D24UnormS8Uint:
|
case Format.D24UnormS8Uint:
|
||||||
case Format.D24X8Unorm:
|
case Format.S8UintD24Unorm:
|
||||||
case Format.D32Float:
|
case Format.D32Float:
|
||||||
case Format.D32FloatS8Uint:
|
case Format.D32FloatS8Uint:
|
||||||
case Format.S8Uint:
|
case Format.S8Uint:
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Types
|
||||||
ZetaFormat.D16Unorm => new FormatInfo(Format.D16Unorm, 1, 1, 2, 1),
|
ZetaFormat.D16Unorm => new FormatInfo(Format.D16Unorm, 1, 1, 2, 1),
|
||||||
ZetaFormat.D24UnormS8Uint => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2),
|
ZetaFormat.D24UnormS8Uint => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2),
|
||||||
ZetaFormat.D24Unorm => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 1),
|
ZetaFormat.D24Unorm => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 1),
|
||||||
ZetaFormat.S8UintD24Unorm => new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2),
|
ZetaFormat.S8UintD24Unorm => new FormatInfo(Format.S8UintD24Unorm, 1, 1, 4, 2),
|
||||||
ZetaFormat.S8Uint => new FormatInfo(Format.S8Uint, 1, 1, 1, 1),
|
ZetaFormat.S8Uint => new FormatInfo(Format.S8Uint, 1, 1, 1, 1),
|
||||||
ZetaFormat.D32FloatS8Uint => new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2),
|
ZetaFormat.D32FloatS8Uint => new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2),
|
||||||
_ => FormatInfo.Default
|
_ => FormatInfo.Default
|
||||||
|
|
|
@ -55,6 +55,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||||
{ 0x24a0e, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) },
|
{ 0x24a0e, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) },
|
||||||
{ 0x24a29, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) },
|
{ 0x24a29, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) },
|
||||||
{ 0x48a29, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) },
|
{ 0x48a29, new FormatInfo(Format.D24UnormS8Uint, 1, 1, 4, 2) },
|
||||||
|
{ 0x4912b, new FormatInfo(Format.S8UintD24Unorm, 1, 1, 4, 2) },
|
||||||
{ 0x25385, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) },
|
{ 0x25385, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) },
|
||||||
{ 0x253b0, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) },
|
{ 0x253b0, new FormatInfo(Format.D32FloatS8Uint, 1, 1, 8, 2) },
|
||||||
{ 0xa4908, new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4) },
|
{ 0xa4908, new FormatInfo(Format.R8G8B8A8Srgb, 1, 1, 4, 4) },
|
||||||
|
|
|
@ -203,7 +203,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
|
if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
|
||||||
lhs.FormatInfo.Format == Format.D24X8Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
|
lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
|
||||||
{
|
{
|
||||||
return TextureMatchQuality.FormatAlias;
|
return TextureMatchQuality.FormatAlias;
|
||||||
}
|
}
|
||||||
|
|
|
@ -362,7 +362,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||||
return DepthStencilMode.Depth;
|
return DepthStencilMode.Depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format == Format.D24X8Unorm || format == Format.D24UnormS8Uint)
|
if (format == Format.D24UnormS8Uint)
|
||||||
{
|
{
|
||||||
return component == SwizzleComponent.Red
|
return component == SwizzleComponent.Red
|
||||||
? DepthStencilMode.Stencil
|
? DepthStencilMode.Stencil
|
||||||
|
|
|
@ -6,15 +6,15 @@ namespace Ryujinx.Graphics.OpenGL
|
||||||
{
|
{
|
||||||
struct FormatTable
|
struct FormatTable
|
||||||
{
|
{
|
||||||
private static FormatInfo[] Table;
|
private static FormatInfo[] _table;
|
||||||
private static SizedInternalFormat[] TableImage;
|
private static SizedInternalFormat[] _tableImage;
|
||||||
|
|
||||||
static FormatTable()
|
static FormatTable()
|
||||||
{
|
{
|
||||||
int tableSize = Enum.GetNames<Format>().Length;
|
int tableSize = Enum.GetNames<Format>().Length;
|
||||||
|
|
||||||
Table = new FormatInfo[tableSize];
|
_table = new FormatInfo[tableSize];
|
||||||
TableImage = new SizedInternalFormat[tableSize];
|
_tableImage = new SizedInternalFormat[tableSize];
|
||||||
|
|
||||||
Add(Format.R8Unorm, new FormatInfo(1, true, false, All.R8, PixelFormat.Red, PixelType.UnsignedByte));
|
Add(Format.R8Unorm, new FormatInfo(1, true, false, All.R8, PixelFormat.Red, PixelType.UnsignedByte));
|
||||||
Add(Format.R8Snorm, new FormatInfo(1, true, false, All.R8Snorm, PixelFormat.Red, PixelType.Byte));
|
Add(Format.R8Snorm, new FormatInfo(1, true, false, All.R8Snorm, PixelFormat.Red, PixelType.Byte));
|
||||||
|
@ -66,7 +66,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||||
Add(Format.R32G32B32A32Sint, new FormatInfo(4, false, false, All.Rgba32i, PixelFormat.RgbaInteger, PixelType.Int));
|
Add(Format.R32G32B32A32Sint, new FormatInfo(4, false, false, All.Rgba32i, PixelFormat.RgbaInteger, PixelType.Int));
|
||||||
Add(Format.S8Uint, new FormatInfo(1, false, false, All.StencilIndex8, PixelFormat.StencilIndex, PixelType.UnsignedByte));
|
Add(Format.S8Uint, new FormatInfo(1, false, false, All.StencilIndex8, PixelFormat.StencilIndex, PixelType.UnsignedByte));
|
||||||
Add(Format.D16Unorm, new FormatInfo(1, false, false, All.DepthComponent16, PixelFormat.DepthComponent, PixelType.UnsignedShort));
|
Add(Format.D16Unorm, new FormatInfo(1, false, false, All.DepthComponent16, PixelFormat.DepthComponent, PixelType.UnsignedShort));
|
||||||
Add(Format.D24X8Unorm, new FormatInfo(1, false, false, All.DepthComponent24, PixelFormat.DepthComponent, PixelType.UnsignedInt));
|
Add(Format.S8UintD24Unorm, new FormatInfo(1, false, false, All.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248));
|
||||||
Add(Format.D32Float, new FormatInfo(1, false, false, All.DepthComponent32f, PixelFormat.DepthComponent, PixelType.Float));
|
Add(Format.D32Float, new FormatInfo(1, false, false, All.DepthComponent32f, PixelFormat.DepthComponent, PixelType.Float));
|
||||||
Add(Format.D24UnormS8Uint, new FormatInfo(1, false, false, All.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248));
|
Add(Format.D24UnormS8Uint, new FormatInfo(1, false, false, All.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248));
|
||||||
Add(Format.D32FloatS8Uint, new FormatInfo(1, false, false, All.Depth32fStencil8, PixelFormat.DepthStencil, PixelType.Float32UnsignedInt248Rev));
|
Add(Format.D32FloatS8Uint, new FormatInfo(1, false, false, All.Depth32fStencil8, PixelFormat.DepthStencil, PixelType.Float32UnsignedInt248Rev));
|
||||||
|
@ -218,22 +218,22 @@ namespace Ryujinx.Graphics.OpenGL
|
||||||
|
|
||||||
private static void Add(Format format, FormatInfo info)
|
private static void Add(Format format, FormatInfo info)
|
||||||
{
|
{
|
||||||
Table[(int)format] = info;
|
_table[(int)format] = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Add(Format format, SizedInternalFormat sif)
|
private static void Add(Format format, SizedInternalFormat sif)
|
||||||
{
|
{
|
||||||
TableImage[(int)format] = sif;
|
_tableImage[(int)format] = sif;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FormatInfo GetFormatInfo(Format format)
|
public static FormatInfo GetFormatInfo(Format format)
|
||||||
{
|
{
|
||||||
return Table[(int)format];
|
return _table[(int)format];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SizedInternalFormat GetImageFormat(Format format)
|
public static SizedInternalFormat GetImageFormat(Format format)
|
||||||
{
|
{
|
||||||
return TableImage[(int)format];
|
return _tableImage[(int)format];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,14 +127,13 @@ namespace Ryujinx.Graphics.OpenGL
|
||||||
private static bool IsPackedDepthStencilFormat(Format format)
|
private static bool IsPackedDepthStencilFormat(Format format)
|
||||||
{
|
{
|
||||||
return format == Format.D24UnormS8Uint ||
|
return format == Format.D24UnormS8Uint ||
|
||||||
format == Format.D32FloatS8Uint;
|
format == Format.D32FloatS8Uint ||
|
||||||
|
format == Format.S8UintD24Unorm;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsDepthOnlyFormat(Format format)
|
private static bool IsDepthOnlyFormat(Format format)
|
||||||
{
|
{
|
||||||
return format == Format.D16Unorm ||
|
return format == Format.D16Unorm || format == Format.D32Float;
|
||||||
format == Format.D24X8Unorm ||
|
|
||||||
format == Format.D32Float;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
149
Ryujinx.Graphics.OpenGL/Image/FormatConverter.cs
Normal file
149
Ryujinx.Graphics.OpenGL/Image/FormatConverter.cs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Intrinsics;
|
||||||
|
using System.Runtime.Intrinsics.X86;
|
||||||
|
|
||||||
|
namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
|
{
|
||||||
|
static class FormatConverter
|
||||||
|
{
|
||||||
|
public unsafe static byte[] ConvertS8D24ToD24S8(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
byte[] output = new byte[data.Length];
|
||||||
|
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
if (Avx2.IsSupported)
|
||||||
|
{
|
||||||
|
var mask = Vector256.Create(
|
||||||
|
(byte)3, (byte)0, (byte)1, (byte)2,
|
||||||
|
(byte)7, (byte)4, (byte)5, (byte)6,
|
||||||
|
(byte)11, (byte)8, (byte)9, (byte)10,
|
||||||
|
(byte)15, (byte)12, (byte)13, (byte)14,
|
||||||
|
(byte)19, (byte)16, (byte)17, (byte)18,
|
||||||
|
(byte)23, (byte)20, (byte)21, (byte)22,
|
||||||
|
(byte)27, (byte)24, (byte)25, (byte)26,
|
||||||
|
(byte)31, (byte)28, (byte)29, (byte)30);
|
||||||
|
|
||||||
|
int sizeAligned = data.Length & ~31;
|
||||||
|
|
||||||
|
fixed (byte* pInput = data, pOutput = output)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < sizeAligned; i += 32)
|
||||||
|
{
|
||||||
|
var dataVec = Avx.LoadVector256(pInput + i);
|
||||||
|
|
||||||
|
dataVec = Avx2.Shuffle(dataVec, mask);
|
||||||
|
|
||||||
|
Avx.Store(pOutput + i, dataVec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = sizeAligned;
|
||||||
|
}
|
||||||
|
else if (Ssse3.IsSupported)
|
||||||
|
{
|
||||||
|
var mask = Vector128.Create(
|
||||||
|
(byte)3, (byte)0, (byte)1, (byte)2,
|
||||||
|
(byte)7, (byte)4, (byte)5, (byte)6,
|
||||||
|
(byte)11, (byte)8, (byte)9, (byte)10,
|
||||||
|
(byte)15, (byte)12, (byte)13, (byte)14);
|
||||||
|
|
||||||
|
int sizeAligned = data.Length & ~15;
|
||||||
|
|
||||||
|
fixed (byte* pInput = data, pOutput = output)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < sizeAligned; i += 16)
|
||||||
|
{
|
||||||
|
var dataVec = Sse2.LoadVector128(pInput + i);
|
||||||
|
|
||||||
|
dataVec = Ssse3.Shuffle(dataVec, mask);
|
||||||
|
|
||||||
|
Sse2.Store(pOutput + i, dataVec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = sizeAligned;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outSpan = MemoryMarshal.Cast<byte, uint>(output);
|
||||||
|
var dataSpan = MemoryMarshal.Cast<byte, uint>(data);
|
||||||
|
for (int i = start / sizeof(uint); i < dataSpan.Length; i++)
|
||||||
|
{
|
||||||
|
outSpan[i] = BitOperations.RotateLeft(dataSpan[i], 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe static byte[] ConvertD24S8ToS8D24(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
byte[] output = new byte[data.Length];
|
||||||
|
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
if (Avx2.IsSupported)
|
||||||
|
{
|
||||||
|
var mask = Vector256.Create(
|
||||||
|
(byte)1, (byte)2, (byte)3, (byte)0,
|
||||||
|
(byte)5, (byte)6, (byte)7, (byte)4,
|
||||||
|
(byte)9, (byte)10, (byte)11, (byte)8,
|
||||||
|
(byte)13, (byte)14, (byte)15, (byte)12,
|
||||||
|
(byte)17, (byte)18, (byte)19, (byte)16,
|
||||||
|
(byte)21, (byte)22, (byte)23, (byte)20,
|
||||||
|
(byte)25, (byte)26, (byte)27, (byte)24,
|
||||||
|
(byte)29, (byte)30, (byte)31, (byte)28);
|
||||||
|
|
||||||
|
int sizeAligned = data.Length & ~31;
|
||||||
|
|
||||||
|
fixed (byte* pInput = data, pOutput = output)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < sizeAligned; i += 32)
|
||||||
|
{
|
||||||
|
var dataVec = Avx.LoadVector256(pInput + i);
|
||||||
|
|
||||||
|
dataVec = Avx2.Shuffle(dataVec, mask);
|
||||||
|
|
||||||
|
Avx.Store(pOutput + i, dataVec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = sizeAligned;
|
||||||
|
}
|
||||||
|
else if (Ssse3.IsSupported)
|
||||||
|
{
|
||||||
|
var mask = Vector128.Create(
|
||||||
|
(byte)1, (byte)2, (byte)3, (byte)0,
|
||||||
|
(byte)5, (byte)6, (byte)7, (byte)4,
|
||||||
|
(byte)9, (byte)10, (byte)11, (byte)8,
|
||||||
|
(byte)13, (byte)14, (byte)15, (byte)12);
|
||||||
|
|
||||||
|
int sizeAligned = data.Length & ~15;
|
||||||
|
|
||||||
|
fixed (byte* pInput = data, pOutput = output)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < sizeAligned; i += 16)
|
||||||
|
{
|
||||||
|
var dataVec = Sse2.LoadVector128(pInput + i);
|
||||||
|
|
||||||
|
dataVec = Ssse3.Shuffle(dataVec, mask);
|
||||||
|
|
||||||
|
Sse2.Store(pOutput + i, dataVec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = sizeAligned;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outSpan = MemoryMarshal.Cast<byte, uint>(output);
|
||||||
|
var dataSpan = MemoryMarshal.Cast<byte, uint>(data);
|
||||||
|
for (int i = start / sizeof(uint); i < dataSpan.Length; i++)
|
||||||
|
{
|
||||||
|
outSpan[i] = BitOperations.RotateRight(dataSpan[i], 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -291,7 +291,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
|
|
||||||
private static ClearBufferMask GetMask(Format format)
|
private static ClearBufferMask GetMask(Format format)
|
||||||
{
|
{
|
||||||
if (format == Format.D24UnormS8Uint || format == Format.D32FloatS8Uint)
|
if (format == Format.D24UnormS8Uint || format == Format.D32FloatS8Uint || format == Format.S8UintD24Unorm)
|
||||||
{
|
{
|
||||||
return ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit;
|
return ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit;
|
||||||
}
|
}
|
||||||
|
@ -311,9 +311,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
|
|
||||||
private static bool IsDepthOnly(Format format)
|
private static bool IsDepthOnly(Format format)
|
||||||
{
|
{
|
||||||
return format == Format.D16Unorm ||
|
return format == Format.D16Unorm || format == Format.D32Float;
|
||||||
format == Format.D24X8Unorm ||
|
|
||||||
format == Format.D32Float;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextureView BgraSwap(TextureView from)
|
public TextureView BgraSwap(TextureView from)
|
||||||
|
|
|
@ -140,9 +140,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
size += Info.GetMipSize(level);
|
size += Info.GetMipSize(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<byte> data;
|
||||||
|
|
||||||
if (HwCapabilities.UsePersistentBufferForFlush)
|
if (HwCapabilities.UsePersistentBufferForFlush)
|
||||||
{
|
{
|
||||||
return _renderer.PersistentBuffers.Default.GetTextureData(this, size);
|
data = _renderer.PersistentBuffers.Default.GetTextureData(this, size);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -150,8 +152,15 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
|
|
||||||
WriteTo(target);
|
WriteTo(target);
|
||||||
|
|
||||||
return new ReadOnlySpan<byte>(target.ToPointer(), size);
|
data = new ReadOnlySpan<byte>(target.ToPointer(), size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Format == Format.S8UintD24Unorm)
|
||||||
|
{
|
||||||
|
data = FormatConverter.ConvertD24S8ToS8D24(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ReadOnlySpan<byte> GetData(int layer, int level)
|
public unsafe ReadOnlySpan<byte> GetData(int layer, int level)
|
||||||
|
@ -285,6 +294,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
|
|
||||||
public void SetData(ReadOnlySpan<byte> data)
|
public void SetData(ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
|
if (Format == Format.S8UintD24Unorm)
|
||||||
|
{
|
||||||
|
data = FormatConverter.ConvertS8D24ToD24S8(data);
|
||||||
|
}
|
||||||
|
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
fixed (byte* ptr = data)
|
fixed (byte* ptr = data)
|
||||||
|
@ -296,6 +310,11 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
||||||
|
|
||||||
public void SetData(ReadOnlySpan<byte> data, int layer, int level)
|
public void SetData(ReadOnlySpan<byte> data, int layer, int level)
|
||||||
{
|
{
|
||||||
|
if (Format == Format.S8UintD24Unorm)
|
||||||
|
{
|
||||||
|
data = FormatConverter.ConvertS8D24ToD24S8(data);
|
||||||
|
}
|
||||||
|
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
fixed (byte* ptr = data)
|
fixed (byte* ptr = data)
|
||||||
|
|
Loading…
Reference in a new issue