mirror of
https://git.naxdy.org/Mirror/Ryujinx.git
synced 2025-02-22 09:03:36 +00:00
Take componentMask and scissor into account when clearing framebuffer attachments
This commit is contained in:
parent
746eded2cf
commit
80d72504d4
7 changed files with 183 additions and 25 deletions
|
@ -17,7 +17,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
|
||||||
{
|
{
|
||||||
public const int ShaderStateIndex = 16;
|
public const int ShaderStateIndex = 16;
|
||||||
public const int RasterizerStateIndex = 15;
|
public const int RasterizerStateIndex = 15;
|
||||||
public const int ScissorStateIndex = 17;
|
public const int ScissorStateIndex = 18;
|
||||||
public const int VertexBufferStateIndex = 0;
|
public const int VertexBufferStateIndex = 0;
|
||||||
public const int PrimitiveRestartStateIndex = 12;
|
public const int PrimitiveRestartStateIndex = 12;
|
||||||
|
|
||||||
|
|
|
@ -121,14 +121,29 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
AttachmentsCount = count;
|
AttachmentsCount = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Auto<DisposableImageView> GetAttachment(int index)
|
||||||
|
{
|
||||||
|
if ((uint)index >= _attachments.Length)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _attachments[index];
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsValidTextureView(ITexture texture)
|
private static bool IsValidTextureView(ITexture texture)
|
||||||
{
|
{
|
||||||
return texture is TextureView view && view.Valid;
|
return texture is TextureView view && view.Valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClearRect GetClearRect()
|
public ClearRect GetClearRect(Rectangle<int> scissor)
|
||||||
{
|
{
|
||||||
return new ClearRect(new Rect2D(null, new Extent2D(Width, Height)), 0, Layers);
|
int x = scissor.X;
|
||||||
|
int y = scissor.Y;
|
||||||
|
int width = Math.Min((int)Width - scissor.X, scissor.Width);
|
||||||
|
int height = Math.Min((int)Height - scissor.Y, scissor.Height);
|
||||||
|
|
||||||
|
return new ClearRect(new Rect2D(new Offset2D(x, y), new Extent2D((uint)width, (uint)height)), 0, Layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe Auto<DisposableFramebuffer> Create(Vk api, CommandBufferScoped cbs, Auto<DisposableRenderPass> renderPass)
|
public unsafe Auto<DisposableFramebuffer> Create(Vk api, CommandBufferScoped cbs, Auto<DisposableRenderPass> renderPass)
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
class HelperShader : IDisposable
|
class HelperShader : IDisposable
|
||||||
{
|
{
|
||||||
private const string VertexShaderSource = @"#version 450 core
|
private const string ColorBlitVertexShaderSource = @"#version 450 core
|
||||||
|
|
||||||
layout (std140, binding = 1) uniform tex_coord_in
|
layout (std140, binding = 1) uniform tex_coord_in
|
||||||
{
|
{
|
||||||
|
@ -41,7 +41,7 @@ void main()
|
||||||
colour = texture(tex, tex_coord);
|
colour = texture(tex, tex_coord);
|
||||||
}";
|
}";
|
||||||
|
|
||||||
private const string ClearAlphaFragmentShaderSource = @"#version 450 core
|
private const string ColorBlitClearAlphaFragmentShaderSource = @"#version 450 core
|
||||||
|
|
||||||
layout (binding = 32, set = 2) uniform sampler2D tex;
|
layout (binding = 32, set = 2) uniform sampler2D tex;
|
||||||
|
|
||||||
|
@ -53,15 +53,47 @@ void main()
|
||||||
colour = vec4(texture(tex, tex_coord).rgb, 1.0f);
|
colour = vec4(texture(tex, tex_coord).rgb, 1.0f);
|
||||||
}";
|
}";
|
||||||
|
|
||||||
private readonly PipelineBlit _pipeline;
|
private const string ColorClearVertexShaderSource = @"#version 450 core
|
||||||
|
|
||||||
|
layout (std140, binding = 1) uniform clear_colour_in
|
||||||
|
{
|
||||||
|
vec4 clear_colour_in_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout (location = 0) out vec4 clear_colour;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
int low = gl_VertexIndex & 1;
|
||||||
|
int high = gl_VertexIndex >> 1;
|
||||||
|
clear_colour = clear_colour_in_data;
|
||||||
|
gl_Position.x = (float(low) - 0.5f) * 2.0f;
|
||||||
|
gl_Position.y = (float(high) - 0.5f) * 2.0f;
|
||||||
|
gl_Position.z = 0.0f;
|
||||||
|
gl_Position.w = 1.0f;
|
||||||
|
}";
|
||||||
|
|
||||||
|
private const string ColorClearFragmentShaderSource = @"#version 450 core
|
||||||
|
|
||||||
|
layout (location = 0) in vec4 clear_colour;
|
||||||
|
layout (location = 0) out vec4 colour;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
colour = clear_colour;
|
||||||
|
}";
|
||||||
|
|
||||||
|
|
||||||
|
private readonly PipelineHelperShader _pipeline;
|
||||||
private readonly ISampler _samplerLinear;
|
private readonly ISampler _samplerLinear;
|
||||||
private readonly ISampler _samplerNearest;
|
private readonly ISampler _samplerNearest;
|
||||||
private readonly IProgram _programColorBlit;
|
private readonly IProgram _programColorBlit;
|
||||||
private readonly IProgram _programClearAlpha;
|
private readonly IProgram _programColorBlitClearAlpha;
|
||||||
|
private readonly IProgram _programColorClear;
|
||||||
|
|
||||||
public HelperShader(VulkanGraphicsDevice gd, Device device)
|
public HelperShader(VulkanGraphicsDevice gd, Device device)
|
||||||
{
|
{
|
||||||
_pipeline = new PipelineBlit(gd, device);
|
_pipeline = new PipelineHelperShader(gd, device);
|
||||||
|
|
||||||
static GAL.SamplerCreateInfo GetSamplerCreateInfo(MinFilter minFilter, MagFilter magFilter)
|
static GAL.SamplerCreateInfo GetSamplerCreateInfo(MinFilter minFilter, MagFilter magFilter)
|
||||||
{
|
{
|
||||||
|
@ -100,12 +132,25 @@ void main()
|
||||||
Array.Empty<int>(),
|
Array.Empty<int>(),
|
||||||
Array.Empty<int>());
|
Array.Empty<int>());
|
||||||
|
|
||||||
var vertexShader = gd.CompileShader(ShaderStage.Vertex, vertexBindings, VertexShaderSource);
|
var colorBlitVertexShader = gd.CompileShader(ShaderStage.Vertex, vertexBindings, ColorBlitVertexShaderSource);
|
||||||
var fragmentShaderColorBlit = gd.CompileShader(ShaderStage.Fragment, fragmentBindings, ColorBlitFragmentShaderSource);
|
var colorBlitFragmentShader = gd.CompileShader(ShaderStage.Fragment, fragmentBindings, ColorBlitFragmentShaderSource);
|
||||||
var fragmentShaderClearAlpha = gd.CompileShader(ShaderStage.Fragment, fragmentBindings, ClearAlphaFragmentShaderSource);
|
var colorBlitClearAlphaFragmentShader = gd.CompileShader(ShaderStage.Fragment, fragmentBindings, ColorBlitClearAlphaFragmentShaderSource);
|
||||||
|
|
||||||
_programColorBlit = gd.CreateProgram(new[] { vertexShader, fragmentShaderColorBlit }, new ShaderInfo(-1));
|
_programColorBlit = gd.CreateProgram(new[] { colorBlitVertexShader, colorBlitFragmentShader }, new ShaderInfo(-1));
|
||||||
_programClearAlpha = gd.CreateProgram(new[] { vertexShader, fragmentShaderClearAlpha }, new ShaderInfo(-1));
|
_programColorBlitClearAlpha = gd.CreateProgram(new[] { colorBlitVertexShader, colorBlitClearAlphaFragmentShader }, new ShaderInfo(-1));
|
||||||
|
|
||||||
|
var fragmentBindings2 = new ShaderBindings(
|
||||||
|
Array.Empty<int>(),
|
||||||
|
Array.Empty<int>(),
|
||||||
|
Array.Empty<int>(),
|
||||||
|
Array.Empty<int>(),
|
||||||
|
Array.Empty<int>(),
|
||||||
|
Array.Empty<int>());
|
||||||
|
|
||||||
|
var colorClearVertexShader = gd.CompileShader(ShaderStage.Vertex, vertexBindings, ColorClearVertexShaderSource);
|
||||||
|
var colorClearFragmentShader = gd.CompileShader(ShaderStage.Fragment, fragmentBindings2, ColorClearFragmentShaderSource);
|
||||||
|
|
||||||
|
_programColorClear = gd.CreateProgram(new[] { colorClearVertexShader, colorClearFragmentShader }, new ShaderInfo(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Blit(
|
public void Blit(
|
||||||
|
@ -200,15 +245,70 @@ void main()
|
||||||
|
|
||||||
scissors[0] = new Rectangle<int>(0, 0, dstWidth, dstHeight);
|
scissors[0] = new Rectangle<int>(0, 0, dstWidth, dstHeight);
|
||||||
|
|
||||||
_pipeline.SetProgram(clearAlpha ? _programClearAlpha : _programColorBlit);
|
_pipeline.SetProgram(clearAlpha ? _programColorBlitClearAlpha : _programColorBlit);
|
||||||
_pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, false, dstFormat);
|
_pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, false, dstFormat);
|
||||||
_pipeline.SetRenderTargetColorMasks(new uint[] { 0xf });
|
_pipeline.SetRenderTargetColorMasks(new uint[] { 0xf });
|
||||||
|
_pipeline.SetScissors(scissors);
|
||||||
|
|
||||||
if (clearAlpha)
|
if (clearAlpha)
|
||||||
{
|
{
|
||||||
_pipeline.ClearRenderTargetColor(0, 0xf, new ColorF(0f, 0f, 0f, 1f));
|
_pipeline.ClearRenderTargetColor(0, new ColorF(0f, 0f, 0f, 1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pipeline.SetViewports(0, viewports);
|
||||||
|
_pipeline.SetPrimitiveTopology(GAL.PrimitiveTopology.TriangleStrip);
|
||||||
|
_pipeline.Draw(4, 1, 0, 0);
|
||||||
|
_pipeline.Finish();
|
||||||
|
|
||||||
|
gd.BufferManager.Delete(bufferHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear(
|
||||||
|
VulkanGraphicsDevice gd,
|
||||||
|
Auto<DisposableImageView> dst,
|
||||||
|
ReadOnlySpan<float> clearColor,
|
||||||
|
uint componentMask,
|
||||||
|
int dstWidth,
|
||||||
|
int dstHeight,
|
||||||
|
VkFormat dstFormat,
|
||||||
|
Rectangle<int> scissor)
|
||||||
|
{
|
||||||
|
gd.FlushAllCommands();
|
||||||
|
|
||||||
|
using var cbs = gd.CommandBufferPool.Rent();
|
||||||
|
|
||||||
|
_pipeline.SetCommandBuffer(cbs);
|
||||||
|
|
||||||
|
const int ClearColorBufferSize = 16;
|
||||||
|
|
||||||
|
var bufferHandle = gd.BufferManager.CreateWithHandle(gd, ClearColorBufferSize, false);
|
||||||
|
|
||||||
|
gd.BufferManager.SetData<float>(bufferHandle, 0, clearColor);
|
||||||
|
|
||||||
|
Span<BufferRange> bufferRanges = stackalloc BufferRange[1];
|
||||||
|
|
||||||
|
bufferRanges[0] = new BufferRange(bufferHandle, 0, ClearColorBufferSize);
|
||||||
|
|
||||||
|
_pipeline.SetUniformBuffers(1, bufferRanges);
|
||||||
|
|
||||||
|
Span<GAL.Viewport> viewports = stackalloc GAL.Viewport[1];
|
||||||
|
|
||||||
|
viewports[0] = new GAL.Viewport(
|
||||||
|
new Rectangle<float>(0, 0, dstWidth, dstHeight),
|
||||||
|
ViewportSwizzle.PositiveX,
|
||||||
|
ViewportSwizzle.PositiveY,
|
||||||
|
ViewportSwizzle.PositiveZ,
|
||||||
|
ViewportSwizzle.PositiveW,
|
||||||
|
0f,
|
||||||
|
1f);
|
||||||
|
|
||||||
|
Span<Rectangle<int>> scissors = stackalloc Rectangle<int>[1];
|
||||||
|
|
||||||
|
scissors[0] = scissor;
|
||||||
|
|
||||||
|
_pipeline.SetProgram(_programColorClear);
|
||||||
|
_pipeline.SetRenderTarget(dst, (uint)dstWidth, (uint)dstHeight, false, dstFormat);
|
||||||
|
_pipeline.SetRenderTargetColorMasks(new uint[] { componentMask });
|
||||||
_pipeline.SetViewports(0, viewports);
|
_pipeline.SetViewports(0, viewports);
|
||||||
_pipeline.SetScissors(scissors);
|
_pipeline.SetScissors(scissors);
|
||||||
_pipeline.SetPrimitiveTopology(GAL.PrimitiveTopology.TriangleStrip);
|
_pipeline.SetPrimitiveTopology(GAL.PrimitiveTopology.TriangleStrip);
|
||||||
|
@ -335,7 +435,7 @@ void main()
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_programClearAlpha.Dispose();
|
_programColorBlitClearAlpha.Dispose();
|
||||||
_programColorBlit.Dispose();
|
_programColorBlit.Dispose();
|
||||||
_samplerNearest.Dispose();
|
_samplerNearest.Dispose();
|
||||||
_samplerLinear.Dispose();
|
_samplerLinear.Dispose();
|
||||||
|
|
|
@ -52,6 +52,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
private BufferState _indexBuffer;
|
private BufferState _indexBuffer;
|
||||||
private readonly BufferState[] _transformFeedbackBuffers;
|
private readonly BufferState[] _transformFeedbackBuffers;
|
||||||
private readonly BufferState[] _vertexBuffers;
|
private readonly BufferState[] _vertexBuffers;
|
||||||
|
protected Rectangle<int> ClearScissor;
|
||||||
|
|
||||||
public SupportBufferUpdater SupportBufferUpdater;
|
public SupportBufferUpdater SupportBufferUpdater;
|
||||||
|
|
||||||
|
@ -88,6 +89,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
_vertexBuffers[0] = new BufferState(emptyVb.GetBuffer(), 0, EmptyVbSize, 0UL);
|
_vertexBuffers[0] = new BufferState(emptyVb.GetBuffer(), 0, EmptyVbSize, 0UL);
|
||||||
_needsVertexBuffersRebind = true;
|
_needsVertexBuffersRebind = true;
|
||||||
|
|
||||||
|
ClearScissor = new Rectangle<int>(0, 0, 0xffff, 0xffff);
|
||||||
|
|
||||||
var defaultScale = new Vector4<float> { X = 1f, Y = 0f, Z = 0f, W = 0f };
|
var defaultScale = new Vector4<float> { X = 1f, Y = 0f, Z = 0f, W = 0f };
|
||||||
new Span<Vector4<float>>(_renderScale).Fill(defaultScale);
|
new Span<Vector4<float>>(_renderScale).Fill(defaultScale);
|
||||||
|
|
||||||
|
@ -140,10 +143,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
Gd.Api.CmdFillBuffer(CommandBuffer, dst, (ulong)offset, (ulong)size, value);
|
Gd.Api.CmdFillBuffer(CommandBuffer, dst, (ulong)offset, (ulong)size, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void ClearRenderTargetColor(int index, uint componentMask, ColorF color)
|
public unsafe void ClearRenderTargetColor(int index, ColorF color)
|
||||||
{
|
{
|
||||||
// TODO: Use componentMask
|
|
||||||
|
|
||||||
if (_framebuffer == null)
|
if (_framebuffer == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
@ -158,7 +159,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
var clearValue = new ClearValue(new ClearColorValue(color.Red, color.Green, color.Blue, color.Alpha));
|
var clearValue = new ClearValue(new ClearColorValue(color.Red, color.Green, color.Blue, color.Alpha));
|
||||||
var attachment = new ClearAttachment(ImageAspectFlags.ImageAspectColorBit, (uint)index, clearValue);
|
var attachment = new ClearAttachment(ImageAspectFlags.ImageAspectColorBit, (uint)index, clearValue);
|
||||||
var clearRect = FramebufferParams?.GetClearRect() ?? default;
|
var clearRect = FramebufferParams?.GetClearRect(ClearScissor) ?? default;
|
||||||
|
|
||||||
Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect);
|
Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect);
|
||||||
}
|
}
|
||||||
|
@ -188,7 +189,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachment = new ClearAttachment(flags, 0, clearValue);
|
var attachment = new ClearAttachment(flags, 0, clearValue);
|
||||||
var clearRect = FramebufferParams?.GetClearRect() ?? default;
|
var clearRect = FramebufferParams?.GetClearRect(ClearScissor) ?? default;
|
||||||
|
|
||||||
Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect);
|
Gd.Api.CmdClearAttachments(CommandBuffer, 1, &attachment, 1, &clearRect);
|
||||||
}
|
}
|
||||||
|
@ -618,6 +619,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
public void SetScissors(ReadOnlySpan<Rectangle<int>> regions)
|
public void SetScissors(ReadOnlySpan<Rectangle<int>> regions)
|
||||||
{
|
{
|
||||||
int count = Math.Min(Constants.MaxViewports, regions.Length);
|
int count = Math.Min(Constants.MaxViewports, regions.Length);
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
ClearScissor = regions[0];
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
|
@ -1047,7 +1052,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
|
|
||||||
private void RecreatePipelineIfNeeded(PipelineBindPoint pbp)
|
private void RecreatePipelineIfNeeded(PipelineBindPoint pbp)
|
||||||
{
|
{
|
||||||
// Take the opportunity to process any pending work requested by other threads.
|
|
||||||
_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.
|
||||||
|
|
|
@ -183,6 +183,45 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
return layouts;
|
return layouts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearRenderTargetColor(int index, uint componentMask, ColorF color)
|
||||||
|
{
|
||||||
|
if (FramebufferParams == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentMask != 0xf)
|
||||||
|
{
|
||||||
|
// We can't use CmdClearAttachments if the clear has a custom scissor or is not writing all components,
|
||||||
|
// because on Vulkan, the pipeline state does not affect clears.
|
||||||
|
var dstTexture = FramebufferParams.GetAttachment(index);
|
||||||
|
if (dstTexture == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<float> clearColor = stackalloc float[4];
|
||||||
|
clearColor[0] = color.Red;
|
||||||
|
clearColor[1] = color.Green;
|
||||||
|
clearColor[2] = color.Blue;
|
||||||
|
clearColor[3] = color.Alpha;
|
||||||
|
|
||||||
|
Gd.HelperShader.Clear(
|
||||||
|
Gd,
|
||||||
|
dstTexture,
|
||||||
|
clearColor,
|
||||||
|
componentMask,
|
||||||
|
(int)FramebufferParams.Width,
|
||||||
|
(int)FramebufferParams.Height,
|
||||||
|
FramebufferParams.AttachmentFormats[index],
|
||||||
|
ClearScissor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClearRenderTargetColor(index, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void EndHostConditionalRendering()
|
public void EndHostConditionalRendering()
|
||||||
{
|
{
|
||||||
if (Gd.Capabilities.SupportsConditionalRendering)
|
if (Gd.Capabilities.SupportsConditionalRendering)
|
||||||
|
|
|
@ -3,9 +3,9 @@ using VkFormat = Silk.NET.Vulkan.Format;
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Vulkan
|
namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
class PipelineBlit : PipelineBase
|
class PipelineHelperShader : PipelineBase
|
||||||
{
|
{
|
||||||
public PipelineBlit(VulkanGraphicsDevice gd, Device device) : base(gd, device)
|
public PipelineHelperShader(VulkanGraphicsDevice gd, Device device) : base(gd, device)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
@ -299,7 +299,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
public NativeArray<PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT> StageRequiredSubgroupSizes;
|
public NativeArray<PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT> StageRequiredSubgroupSizes;
|
||||||
public PipelineLayout PipelineLayout;
|
public PipelineLayout PipelineLayout;
|
||||||
|
|
||||||
public unsafe void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
Stages = new NativeArray<PipelineShaderStageCreateInfo>(Constants.MaxShaderStages);
|
Stages = new NativeArray<PipelineShaderStageCreateInfo>(Constants.MaxShaderStages);
|
||||||
StageRequiredSubgroupSizes = new NativeArray<PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT>(Constants.MaxShaderStages);
|
StageRequiredSubgroupSizes = new NativeArray<PipelineShaderStageRequiredSubgroupSizeCreateInfoEXT>(Constants.MaxShaderStages);
|
||||||
|
|
Loading…
Reference in a new issue