commit
1fca683388
|
@ -723,7 +723,11 @@ public:
|
||||||
StencilOp stencil_back_op_zpass;
|
StencilOp stencil_back_op_zpass;
|
||||||
ComparisonOp stencil_back_func_func;
|
ComparisonOp stencil_back_func_func;
|
||||||
|
|
||||||
INSERT_PADDING_WORDS(0x17);
|
INSERT_PADDING_WORDS(0x4);
|
||||||
|
|
||||||
|
u32 framebuffer_srgb;
|
||||||
|
|
||||||
|
INSERT_PADDING_WORDS(0x12);
|
||||||
|
|
||||||
union {
|
union {
|
||||||
BitField<2, 1, u32> coord_origin;
|
BitField<2, 1, u32> coord_origin;
|
||||||
|
@ -1086,6 +1090,7 @@ ASSERT_REG_POSITION(stencil_back_op_fail, 0x566);
|
||||||
ASSERT_REG_POSITION(stencil_back_op_zfail, 0x567);
|
ASSERT_REG_POSITION(stencil_back_op_zfail, 0x567);
|
||||||
ASSERT_REG_POSITION(stencil_back_op_zpass, 0x568);
|
ASSERT_REG_POSITION(stencil_back_op_zpass, 0x568);
|
||||||
ASSERT_REG_POSITION(stencil_back_func_func, 0x569);
|
ASSERT_REG_POSITION(stencil_back_func_func, 0x569);
|
||||||
|
ASSERT_REG_POSITION(framebuffer_srgb, 0x56E);
|
||||||
ASSERT_REG_POSITION(point_coord_replace, 0x581);
|
ASSERT_REG_POSITION(point_coord_replace, 0x581);
|
||||||
ASSERT_REG_POSITION(code_address, 0x582);
|
ASSERT_REG_POSITION(code_address, 0x582);
|
||||||
ASSERT_REG_POSITION(draw, 0x585);
|
ASSERT_REG_POSITION(draw, 0x585);
|
||||||
|
|
|
@ -418,6 +418,7 @@ void RasterizerOpenGL::ConfigureFramebuffers(bool using_color_fb, bool using_dep
|
||||||
// Bind the framebuffer surfaces
|
// Bind the framebuffer surfaces
|
||||||
state.draw.draw_framebuffer = framebuffer.handle;
|
state.draw.draw_framebuffer = framebuffer.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
state.framebuffer_srgb.enabled = regs.framebuffer_srgb != 0;
|
||||||
|
|
||||||
if (using_color_fb) {
|
if (using_color_fb) {
|
||||||
if (single_color_target) {
|
if (single_color_target) {
|
||||||
|
@ -429,6 +430,9 @@ void RasterizerOpenGL::ConfigureFramebuffers(bool using_color_fb, bool using_dep
|
||||||
// Assume that a surface will be written to if it is used as a framebuffer, even if
|
// Assume that a surface will be written to if it is used as a framebuffer, even if
|
||||||
// the shader doesn't actually write to it.
|
// the shader doesn't actually write to it.
|
||||||
color_surface->MarkAsModified(true, res_cache);
|
color_surface->MarkAsModified(true, res_cache);
|
||||||
|
// Workaround for and issue in nvidia drivers
|
||||||
|
// https://devtalk.nvidia.com/default/topic/776591/opengl/gl_framebuffer_srgb-functions-incorrectly/
|
||||||
|
state.framebuffer_srgb.enabled |= color_surface->GetSurfaceParams().srgb_conversion;
|
||||||
}
|
}
|
||||||
|
|
||||||
glFramebufferTexture2D(
|
glFramebufferTexture2D(
|
||||||
|
@ -446,6 +450,11 @@ void RasterizerOpenGL::ConfigureFramebuffers(bool using_color_fb, bool using_dep
|
||||||
// Assume that a surface will be written to if it is used as a framebuffer, even
|
// Assume that a surface will be written to if it is used as a framebuffer, even
|
||||||
// if the shader doesn't actually write to it.
|
// if the shader doesn't actually write to it.
|
||||||
color_surface->MarkAsModified(true, res_cache);
|
color_surface->MarkAsModified(true, res_cache);
|
||||||
|
// Enable sRGB only for supported formats
|
||||||
|
// Workaround for and issue in nvidia drivers
|
||||||
|
// https://devtalk.nvidia.com/default/topic/776591/opengl/gl_framebuffer_srgb-functions-incorrectly/
|
||||||
|
state.framebuffer_srgb.enabled |=
|
||||||
|
color_surface->GetSurfaceParams().srgb_conversion;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffers[index] = GL_COLOR_ATTACHMENT0 + regs.rt_control.GetMap(index);
|
buffers[index] = GL_COLOR_ATTACHMENT0 + regs.rt_control.GetMap(index);
|
||||||
|
@ -537,7 +546,9 @@ void RasterizerOpenGL::Clear() {
|
||||||
|
|
||||||
ConfigureFramebuffers(use_color, use_depth || use_stencil, false,
|
ConfigureFramebuffers(use_color, use_depth || use_stencil, false,
|
||||||
regs.clear_buffers.RT.Value());
|
regs.clear_buffers.RT.Value());
|
||||||
|
// Copy the sRGB setting to the clear state to avoid problem with
|
||||||
|
// specific driver implementations
|
||||||
|
clear_state.framebuffer_srgb.enabled = state.framebuffer_srgb.enabled;
|
||||||
clear_state.Apply();
|
clear_state.Apply();
|
||||||
|
|
||||||
if (use_color) {
|
if (use_color) {
|
||||||
|
|
|
@ -40,6 +40,10 @@ static bool IsPixelFormatASTC(PixelFormat format) {
|
||||||
case PixelFormat::ASTC_2D_5X4:
|
case PixelFormat::ASTC_2D_5X4:
|
||||||
case PixelFormat::ASTC_2D_8X8:
|
case PixelFormat::ASTC_2D_8X8:
|
||||||
case PixelFormat::ASTC_2D_8X5:
|
case PixelFormat::ASTC_2D_8X5:
|
||||||
|
case PixelFormat::ASTC_2D_4X4_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_5X4_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_8X8_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_8X5_SRGB:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -56,6 +60,14 @@ static std::pair<u32, u32> GetASTCBlockSize(PixelFormat format) {
|
||||||
return {8, 8};
|
return {8, 8};
|
||||||
case PixelFormat::ASTC_2D_8X5:
|
case PixelFormat::ASTC_2D_8X5:
|
||||||
return {8, 5};
|
return {8, 5};
|
||||||
|
case PixelFormat::ASTC_2D_4X4_SRGB:
|
||||||
|
return {4, 4};
|
||||||
|
case PixelFormat::ASTC_2D_5X4_SRGB:
|
||||||
|
return {5, 4};
|
||||||
|
case PixelFormat::ASTC_2D_8X8_SRGB:
|
||||||
|
return {8, 8};
|
||||||
|
case PixelFormat::ASTC_2D_8X5_SRGB:
|
||||||
|
return {8, 5};
|
||||||
default:
|
default:
|
||||||
LOG_CRITICAL(HW_GPU, "Unhandled format: {}", static_cast<u32>(format));
|
LOG_CRITICAL(HW_GPU, "Unhandled format: {}", static_cast<u32>(format));
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
|
@ -108,8 +120,9 @@ std::size_t SurfaceParams::InnerMemorySize(bool layer_only) const {
|
||||||
params.block_width = params.is_tiled ? config.tic.BlockWidth() : 0,
|
params.block_width = params.is_tiled ? config.tic.BlockWidth() : 0,
|
||||||
params.block_height = params.is_tiled ? config.tic.BlockHeight() : 0,
|
params.block_height = params.is_tiled ? config.tic.BlockHeight() : 0,
|
||||||
params.block_depth = params.is_tiled ? config.tic.BlockDepth() : 0,
|
params.block_depth = params.is_tiled ? config.tic.BlockDepth() : 0,
|
||||||
params.pixel_format =
|
params.srgb_conversion = config.tic.IsSrgbConversionEnabled();
|
||||||
PixelFormatFromTextureFormat(config.tic.format, config.tic.r_type.Value());
|
params.pixel_format = PixelFormatFromTextureFormat(config.tic.format, config.tic.r_type.Value(),
|
||||||
|
params.srgb_conversion);
|
||||||
params.component_type = ComponentTypeFromTexture(config.tic.r_type.Value());
|
params.component_type = ComponentTypeFromTexture(config.tic.r_type.Value());
|
||||||
params.type = GetFormatType(params.pixel_format);
|
params.type = GetFormatType(params.pixel_format);
|
||||||
params.width = Common::AlignUp(config.tic.Width(), GetCompressionFactor(params.pixel_format));
|
params.width = Common::AlignUp(config.tic.Width(), GetCompressionFactor(params.pixel_format));
|
||||||
|
@ -166,6 +179,8 @@ std::size_t SurfaceParams::InnerMemorySize(bool layer_only) const {
|
||||||
params.block_height = 1 << config.memory_layout.block_height;
|
params.block_height = 1 << config.memory_layout.block_height;
|
||||||
params.block_depth = 1 << config.memory_layout.block_depth;
|
params.block_depth = 1 << config.memory_layout.block_depth;
|
||||||
params.pixel_format = PixelFormatFromRenderTargetFormat(config.format);
|
params.pixel_format = PixelFormatFromRenderTargetFormat(config.format);
|
||||||
|
params.srgb_conversion = config.format == Tegra::RenderTargetFormat::BGRA8_SRGB ||
|
||||||
|
config.format == Tegra::RenderTargetFormat::RGBA8_SRGB;
|
||||||
params.component_type = ComponentTypeFromRenderTarget(config.format);
|
params.component_type = ComponentTypeFromRenderTarget(config.format);
|
||||||
params.type = GetFormatType(params.pixel_format);
|
params.type = GetFormatType(params.pixel_format);
|
||||||
params.width = config.width;
|
params.width = config.width;
|
||||||
|
@ -201,6 +216,7 @@ std::size_t SurfaceParams::InnerMemorySize(bool layer_only) const {
|
||||||
params.pixel_format = PixelFormatFromDepthFormat(format);
|
params.pixel_format = PixelFormatFromDepthFormat(format);
|
||||||
params.component_type = ComponentTypeFromDepthFormat(format);
|
params.component_type = ComponentTypeFromDepthFormat(format);
|
||||||
params.type = GetFormatType(params.pixel_format);
|
params.type = GetFormatType(params.pixel_format);
|
||||||
|
params.srgb_conversion = false;
|
||||||
params.width = zeta_width;
|
params.width = zeta_width;
|
||||||
params.height = zeta_height;
|
params.height = zeta_height;
|
||||||
params.unaligned_height = zeta_height;
|
params.unaligned_height = zeta_height;
|
||||||
|
@ -224,6 +240,8 @@ std::size_t SurfaceParams::InnerMemorySize(bool layer_only) const {
|
||||||
params.block_height = params.is_tiled ? std::min(config.BlockHeight(), 32U) : 0,
|
params.block_height = params.is_tiled ? std::min(config.BlockHeight(), 32U) : 0,
|
||||||
params.block_depth = params.is_tiled ? std::min(config.BlockDepth(), 32U) : 0,
|
params.block_depth = params.is_tiled ? std::min(config.BlockDepth(), 32U) : 0,
|
||||||
params.pixel_format = PixelFormatFromRenderTargetFormat(config.format);
|
params.pixel_format = PixelFormatFromRenderTargetFormat(config.format);
|
||||||
|
params.srgb_conversion = config.format == Tegra::RenderTargetFormat::BGRA8_SRGB ||
|
||||||
|
config.format == Tegra::RenderTargetFormat::RGBA8_SRGB;
|
||||||
params.component_type = ComponentTypeFromRenderTarget(config.format);
|
params.component_type = ComponentTypeFromRenderTarget(config.format);
|
||||||
params.type = GetFormatType(params.pixel_format);
|
params.type = GetFormatType(params.pixel_format);
|
||||||
params.width = config.width;
|
params.width = config.width;
|
||||||
|
@ -289,7 +307,8 @@ static constexpr std::array<FormatTuple, SurfaceParams::MaxPixelFormat> tex_form
|
||||||
{GL_RG16I, GL_RG_INTEGER, GL_SHORT, ComponentType::SInt, false}, // RG16I
|
{GL_RG16I, GL_RG_INTEGER, GL_SHORT, ComponentType::SInt, false}, // RG16I
|
||||||
{GL_RG16_SNORM, GL_RG, GL_SHORT, ComponentType::SNorm, false}, // RG16S
|
{GL_RG16_SNORM, GL_RG, GL_SHORT, ComponentType::SNorm, false}, // RG16S
|
||||||
{GL_RGB32F, GL_RGB, GL_FLOAT, ComponentType::Float, false}, // RGB32F
|
{GL_RGB32F, GL_RGB, GL_FLOAT, ComponentType::Float, false}, // RGB32F
|
||||||
{GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, ComponentType::UNorm, false}, // SRGBA8
|
{GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, ComponentType::UNorm,
|
||||||
|
false}, // RGBA8_SRGB
|
||||||
{GL_RG8, GL_RG, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // RG8U
|
{GL_RG8, GL_RG, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // RG8U
|
||||||
{GL_RG8, GL_RG, GL_BYTE, ComponentType::SNorm, false}, // RG8S
|
{GL_RG8, GL_RG, GL_BYTE, ComponentType::SNorm, false}, // RG8S
|
||||||
{GL_RG32UI, GL_RG_INTEGER, GL_UNSIGNED_INT, ComponentType::UInt, false}, // RG32UI
|
{GL_RG32UI, GL_RG_INTEGER, GL_UNSIGNED_INT, ComponentType::UInt, false}, // RG32UI
|
||||||
|
@ -297,6 +316,20 @@ static constexpr std::array<FormatTuple, SurfaceParams::MaxPixelFormat> tex_form
|
||||||
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_8X8
|
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_8X8
|
||||||
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_8X5
|
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_8X5
|
||||||
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_5X4
|
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_5X4
|
||||||
|
{GL_SRGB8_ALPHA8, GL_BGRA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // BGRA8
|
||||||
|
// Compressed sRGB formats
|
||||||
|
{GL_COMPRESSED_SRGB_S3TC_DXT1_EXT, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, ComponentType::UNorm,
|
||||||
|
true}, // DXT1_SRGB
|
||||||
|
{GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, ComponentType::UNorm,
|
||||||
|
true}, // DXT23_SRGB
|
||||||
|
{GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, ComponentType::UNorm,
|
||||||
|
true}, // DXT45_SRGB
|
||||||
|
{GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8,
|
||||||
|
ComponentType::UNorm, true}, // BC7U_SRGB
|
||||||
|
{GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_4X4_SRGB
|
||||||
|
{GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_8X8_SRGB
|
||||||
|
{GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_8X5_SRGB
|
||||||
|
{GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, ComponentType::UNorm, false}, // ASTC_2D_5X4_SRGB
|
||||||
|
|
||||||
// Depth formats
|
// Depth formats
|
||||||
{GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, ComponentType::Float, false}, // Z32F
|
{GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, ComponentType::Float, false}, // Z32F
|
||||||
|
@ -361,6 +394,10 @@ static bool IsFormatBCn(PixelFormat format) {
|
||||||
case PixelFormat::BC7U:
|
case PixelFormat::BC7U:
|
||||||
case PixelFormat::BC6H_UF16:
|
case PixelFormat::BC6H_UF16:
|
||||||
case PixelFormat::BC6H_SF16:
|
case PixelFormat::BC6H_SF16:
|
||||||
|
case PixelFormat::DXT1_SRGB:
|
||||||
|
case PixelFormat::DXT23_SRGB:
|
||||||
|
case PixelFormat::DXT45_SRGB:
|
||||||
|
case PixelFormat::BC7U_SRGB:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -432,7 +469,7 @@ static constexpr GLConversionArray morton_to_gl_fns = {
|
||||||
MortonCopy<true, PixelFormat::RG16I>,
|
MortonCopy<true, PixelFormat::RG16I>,
|
||||||
MortonCopy<true, PixelFormat::RG16S>,
|
MortonCopy<true, PixelFormat::RG16S>,
|
||||||
MortonCopy<true, PixelFormat::RGB32F>,
|
MortonCopy<true, PixelFormat::RGB32F>,
|
||||||
MortonCopy<true, PixelFormat::SRGBA8>,
|
MortonCopy<true, PixelFormat::RGBA8_SRGB>,
|
||||||
MortonCopy<true, PixelFormat::RG8U>,
|
MortonCopy<true, PixelFormat::RG8U>,
|
||||||
MortonCopy<true, PixelFormat::RG8S>,
|
MortonCopy<true, PixelFormat::RG8S>,
|
||||||
MortonCopy<true, PixelFormat::RG32UI>,
|
MortonCopy<true, PixelFormat::RG32UI>,
|
||||||
|
@ -440,6 +477,15 @@ static constexpr GLConversionArray morton_to_gl_fns = {
|
||||||
MortonCopy<true, PixelFormat::ASTC_2D_8X8>,
|
MortonCopy<true, PixelFormat::ASTC_2D_8X8>,
|
||||||
MortonCopy<true, PixelFormat::ASTC_2D_8X5>,
|
MortonCopy<true, PixelFormat::ASTC_2D_8X5>,
|
||||||
MortonCopy<true, PixelFormat::ASTC_2D_5X4>,
|
MortonCopy<true, PixelFormat::ASTC_2D_5X4>,
|
||||||
|
MortonCopy<true, PixelFormat::BGRA8_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::DXT1_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::DXT23_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::DXT45_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::BC7U_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::ASTC_2D_4X4_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::ASTC_2D_8X8_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::ASTC_2D_8X5_SRGB>,
|
||||||
|
MortonCopy<true, PixelFormat::ASTC_2D_5X4_SRGB>,
|
||||||
MortonCopy<true, PixelFormat::Z32F>,
|
MortonCopy<true, PixelFormat::Z32F>,
|
||||||
MortonCopy<true, PixelFormat::Z16>,
|
MortonCopy<true, PixelFormat::Z16>,
|
||||||
MortonCopy<true, PixelFormat::Z24S8>,
|
MortonCopy<true, PixelFormat::Z24S8>,
|
||||||
|
@ -491,7 +537,7 @@ static constexpr GLConversionArray gl_to_morton_fns = {
|
||||||
MortonCopy<false, PixelFormat::RG16I>,
|
MortonCopy<false, PixelFormat::RG16I>,
|
||||||
MortonCopy<false, PixelFormat::RG16S>,
|
MortonCopy<false, PixelFormat::RG16S>,
|
||||||
MortonCopy<false, PixelFormat::RGB32F>,
|
MortonCopy<false, PixelFormat::RGB32F>,
|
||||||
MortonCopy<false, PixelFormat::SRGBA8>,
|
MortonCopy<false, PixelFormat::RGBA8_SRGB>,
|
||||||
MortonCopy<false, PixelFormat::RG8U>,
|
MortonCopy<false, PixelFormat::RG8U>,
|
||||||
MortonCopy<false, PixelFormat::RG8S>,
|
MortonCopy<false, PixelFormat::RG8S>,
|
||||||
MortonCopy<false, PixelFormat::RG32UI>,
|
MortonCopy<false, PixelFormat::RG32UI>,
|
||||||
|
@ -499,6 +545,15 @@ static constexpr GLConversionArray gl_to_morton_fns = {
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
|
MortonCopy<false, PixelFormat::BGRA8_SRGB>,
|
||||||
|
MortonCopy<false, PixelFormat::DXT1_SRGB>,
|
||||||
|
MortonCopy<false, PixelFormat::DXT23_SRGB>,
|
||||||
|
MortonCopy<false, PixelFormat::DXT45_SRGB>,
|
||||||
|
MortonCopy<false, PixelFormat::BC7U_SRGB>,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
MortonCopy<false, PixelFormat::Z32F>,
|
MortonCopy<false, PixelFormat::Z32F>,
|
||||||
MortonCopy<false, PixelFormat::Z16>,
|
MortonCopy<false, PixelFormat::Z16>,
|
||||||
MortonCopy<false, PixelFormat::Z24S8>,
|
MortonCopy<false, PixelFormat::Z24S8>,
|
||||||
|
@ -546,6 +601,8 @@ static bool BlitSurface(const Surface& src_surface, const Surface& dst_surface,
|
||||||
OpenGLState state;
|
OpenGLState state;
|
||||||
state.draw.read_framebuffer = read_fb_handle;
|
state.draw.read_framebuffer = read_fb_handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fb_handle;
|
||||||
|
// Set sRGB enabled if the destination surfaces need it
|
||||||
|
state.framebuffer_srgb.enabled = dst_params.srgb_conversion;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
u32 buffers{};
|
u32 buffers{};
|
||||||
|
@ -881,7 +938,11 @@ static void ConvertFormatAsNeeded_LoadGLBuffer(std::vector<u8>& data, PixelForma
|
||||||
case PixelFormat::ASTC_2D_4X4:
|
case PixelFormat::ASTC_2D_4X4:
|
||||||
case PixelFormat::ASTC_2D_8X8:
|
case PixelFormat::ASTC_2D_8X8:
|
||||||
case PixelFormat::ASTC_2D_8X5:
|
case PixelFormat::ASTC_2D_8X5:
|
||||||
case PixelFormat::ASTC_2D_5X4: {
|
case PixelFormat::ASTC_2D_5X4:
|
||||||
|
case PixelFormat::ASTC_2D_4X4_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_8X8_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_8X5_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_5X4_SRGB: {
|
||||||
// Convert ASTC pixel formats to RGBA8, as most desktop GPUs do not support ASTC.
|
// Convert ASTC pixel formats to RGBA8, as most desktop GPUs do not support ASTC.
|
||||||
u32 block_width{};
|
u32 block_width{};
|
||||||
u32 block_height{};
|
u32 block_height{};
|
||||||
|
@ -913,7 +974,9 @@ static void ConvertFormatAsNeeded_FlushGLBuffer(std::vector<u8>& data, PixelForm
|
||||||
case PixelFormat::G8R8U:
|
case PixelFormat::G8R8U:
|
||||||
case PixelFormat::G8R8S:
|
case PixelFormat::G8R8S:
|
||||||
case PixelFormat::ASTC_2D_4X4:
|
case PixelFormat::ASTC_2D_4X4:
|
||||||
case PixelFormat::ASTC_2D_8X8: {
|
case PixelFormat::ASTC_2D_8X8:
|
||||||
|
case PixelFormat::ASTC_2D_4X4_SRGB:
|
||||||
|
case PixelFormat::ASTC_2D_8X8_SRGB: {
|
||||||
LOG_CRITICAL(HW_GPU, "Conversion of format {} after texture flushing is not implemented",
|
LOG_CRITICAL(HW_GPU, "Conversion of format {} after texture flushing is not implemented",
|
||||||
static_cast<u32>(pixel_format));
|
static_cast<u32>(pixel_format));
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
|
|
|
@ -69,7 +69,7 @@ struct SurfaceParams {
|
||||||
RG16I = 37,
|
RG16I = 37,
|
||||||
RG16S = 38,
|
RG16S = 38,
|
||||||
RGB32F = 39,
|
RGB32F = 39,
|
||||||
SRGBA8 = 40,
|
RGBA8_SRGB = 40,
|
||||||
RG8U = 41,
|
RG8U = 41,
|
||||||
RG8S = 42,
|
RG8S = 42,
|
||||||
RG32UI = 43,
|
RG32UI = 43,
|
||||||
|
@ -77,19 +77,28 @@ struct SurfaceParams {
|
||||||
ASTC_2D_8X8 = 45,
|
ASTC_2D_8X8 = 45,
|
||||||
ASTC_2D_8X5 = 46,
|
ASTC_2D_8X5 = 46,
|
||||||
ASTC_2D_5X4 = 47,
|
ASTC_2D_5X4 = 47,
|
||||||
|
BGRA8_SRGB = 48,
|
||||||
|
DXT1_SRGB = 49,
|
||||||
|
DXT23_SRGB = 50,
|
||||||
|
DXT45_SRGB = 51,
|
||||||
|
BC7U_SRGB = 52,
|
||||||
|
ASTC_2D_4X4_SRGB = 53,
|
||||||
|
ASTC_2D_8X8_SRGB = 54,
|
||||||
|
ASTC_2D_8X5_SRGB = 55,
|
||||||
|
ASTC_2D_5X4_SRGB = 56,
|
||||||
|
|
||||||
MaxColorFormat,
|
MaxColorFormat,
|
||||||
|
|
||||||
// Depth formats
|
// Depth formats
|
||||||
Z32F = 48,
|
Z32F = 57,
|
||||||
Z16 = 49,
|
Z16 = 58,
|
||||||
|
|
||||||
MaxDepthFormat,
|
MaxDepthFormat,
|
||||||
|
|
||||||
// DepthStencil formats
|
// DepthStencil formats
|
||||||
Z24S8 = 50,
|
Z24S8 = 59,
|
||||||
S8Z24 = 51,
|
S8Z24 = 60,
|
||||||
Z32FS8 = 52,
|
Z32FS8 = 61,
|
||||||
|
|
||||||
MaxDepthStencilFormat,
|
MaxDepthStencilFormat,
|
||||||
|
|
||||||
|
@ -236,7 +245,7 @@ struct SurfaceParams {
|
||||||
1, // RG16I
|
1, // RG16I
|
||||||
1, // RG16S
|
1, // RG16S
|
||||||
1, // RGB32F
|
1, // RGB32F
|
||||||
1, // SRGBA8
|
1, // RGBA8_SRGB
|
||||||
1, // RG8U
|
1, // RG8U
|
||||||
1, // RG8S
|
1, // RG8S
|
||||||
1, // RG32UI
|
1, // RG32UI
|
||||||
|
@ -244,6 +253,15 @@ struct SurfaceParams {
|
||||||
4, // ASTC_2D_8X8
|
4, // ASTC_2D_8X8
|
||||||
4, // ASTC_2D_8X5
|
4, // ASTC_2D_8X5
|
||||||
4, // ASTC_2D_5X4
|
4, // ASTC_2D_5X4
|
||||||
|
1, // BGRA8_SRGB
|
||||||
|
4, // DXT1_SRGB
|
||||||
|
4, // DXT23_SRGB
|
||||||
|
4, // DXT45_SRGB
|
||||||
|
4, // BC7U_SRGB
|
||||||
|
4, // ASTC_2D_4X4_SRGB
|
||||||
|
4, // ASTC_2D_8X8_SRGB
|
||||||
|
4, // ASTC_2D_8X5_SRGB
|
||||||
|
4, // ASTC_2D_5X4_SRGB
|
||||||
1, // Z32F
|
1, // Z32F
|
||||||
1, // Z16
|
1, // Z16
|
||||||
1, // Z24S8
|
1, // Z24S8
|
||||||
|
@ -299,7 +317,7 @@ struct SurfaceParams {
|
||||||
1, // RG16I
|
1, // RG16I
|
||||||
1, // RG16S
|
1, // RG16S
|
||||||
1, // RGB32F
|
1, // RGB32F
|
||||||
1, // SRGBA8
|
1, // RGBA8_SRGB
|
||||||
1, // RG8U
|
1, // RG8U
|
||||||
1, // RG8S
|
1, // RG8S
|
||||||
1, // RG32UI
|
1, // RG32UI
|
||||||
|
@ -307,6 +325,15 @@ struct SurfaceParams {
|
||||||
8, // ASTC_2D_8X8
|
8, // ASTC_2D_8X8
|
||||||
5, // ASTC_2D_8X5
|
5, // ASTC_2D_8X5
|
||||||
4, // ASTC_2D_5X4
|
4, // ASTC_2D_5X4
|
||||||
|
1, // BGRA8_SRGB
|
||||||
|
4, // DXT1_SRGB
|
||||||
|
4, // DXT23_SRGB
|
||||||
|
4, // DXT45_SRGB
|
||||||
|
4, // BC7U_SRGB
|
||||||
|
4, // ASTC_2D_4X4_SRGB
|
||||||
|
8, // ASTC_2D_8X8_SRGB
|
||||||
|
5, // ASTC_2D_8X5_SRGB
|
||||||
|
4, // ASTC_2D_5X4_SRGB
|
||||||
1, // Z32F
|
1, // Z32F
|
||||||
1, // Z16
|
1, // Z16
|
||||||
1, // Z24S8
|
1, // Z24S8
|
||||||
|
@ -362,7 +389,7 @@ struct SurfaceParams {
|
||||||
32, // RG16I
|
32, // RG16I
|
||||||
32, // RG16S
|
32, // RG16S
|
||||||
96, // RGB32F
|
96, // RGB32F
|
||||||
32, // SRGBA8
|
32, // RGBA8_SRGB
|
||||||
16, // RG8U
|
16, // RG8U
|
||||||
16, // RG8S
|
16, // RG8S
|
||||||
64, // RG32UI
|
64, // RG32UI
|
||||||
|
@ -370,6 +397,15 @@ struct SurfaceParams {
|
||||||
16, // ASTC_2D_8X8
|
16, // ASTC_2D_8X8
|
||||||
32, // ASTC_2D_8X5
|
32, // ASTC_2D_8X5
|
||||||
32, // ASTC_2D_5X4
|
32, // ASTC_2D_5X4
|
||||||
|
32, // BGRA8_SRGB
|
||||||
|
64, // DXT1_SRGB
|
||||||
|
128, // DXT23_SRGB
|
||||||
|
128, // DXT45_SRGB
|
||||||
|
128, // BC7U
|
||||||
|
32, // ASTC_2D_4X4_SRGB
|
||||||
|
16, // ASTC_2D_8X8_SRGB
|
||||||
|
32, // ASTC_2D_8X5_SRGB
|
||||||
|
32, // ASTC_2D_5X4_SRGB
|
||||||
32, // Z32F
|
32, // Z32F
|
||||||
16, // Z16
|
16, // Z16
|
||||||
32, // Z24S8
|
32, // Z24S8
|
||||||
|
@ -408,6 +444,7 @@ struct SurfaceParams {
|
||||||
// TODO (Hexagon12): Converting SRGBA to RGBA is a hack and doesn't completely correct the
|
// TODO (Hexagon12): Converting SRGBA to RGBA is a hack and doesn't completely correct the
|
||||||
// gamma.
|
// gamma.
|
||||||
case Tegra::RenderTargetFormat::RGBA8_SRGB:
|
case Tegra::RenderTargetFormat::RGBA8_SRGB:
|
||||||
|
return PixelFormat::RGBA8_SRGB;
|
||||||
case Tegra::RenderTargetFormat::RGBA8_UNORM:
|
case Tegra::RenderTargetFormat::RGBA8_UNORM:
|
||||||
return PixelFormat::ABGR8U;
|
return PixelFormat::ABGR8U;
|
||||||
case Tegra::RenderTargetFormat::RGBA8_SNORM:
|
case Tegra::RenderTargetFormat::RGBA8_SNORM:
|
||||||
|
@ -415,6 +452,7 @@ struct SurfaceParams {
|
||||||
case Tegra::RenderTargetFormat::RGBA8_UINT:
|
case Tegra::RenderTargetFormat::RGBA8_UINT:
|
||||||
return PixelFormat::ABGR8UI;
|
return PixelFormat::ABGR8UI;
|
||||||
case Tegra::RenderTargetFormat::BGRA8_SRGB:
|
case Tegra::RenderTargetFormat::BGRA8_SRGB:
|
||||||
|
return PixelFormat::BGRA8_SRGB;
|
||||||
case Tegra::RenderTargetFormat::BGRA8_UNORM:
|
case Tegra::RenderTargetFormat::BGRA8_UNORM:
|
||||||
return PixelFormat::BGRA8;
|
return PixelFormat::BGRA8;
|
||||||
case Tegra::RenderTargetFormat::RGB10_A2_UNORM:
|
case Tegra::RenderTargetFormat::RGB10_A2_UNORM:
|
||||||
|
@ -478,10 +516,14 @@ struct SurfaceParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
static PixelFormat PixelFormatFromTextureFormat(Tegra::Texture::TextureFormat format,
|
static PixelFormat PixelFormatFromTextureFormat(Tegra::Texture::TextureFormat format,
|
||||||
Tegra::Texture::ComponentType component_type) {
|
Tegra::Texture::ComponentType component_type,
|
||||||
|
bool is_srgb) {
|
||||||
// TODO(Subv): Properly implement this
|
// TODO(Subv): Properly implement this
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case Tegra::Texture::TextureFormat::A8R8G8B8:
|
case Tegra::Texture::TextureFormat::A8R8G8B8:
|
||||||
|
if (is_srgb) {
|
||||||
|
return PixelFormat::RGBA8_SRGB;
|
||||||
|
}
|
||||||
switch (component_type) {
|
switch (component_type) {
|
||||||
case Tegra::Texture::ComponentType::UNORM:
|
case Tegra::Texture::ComponentType::UNORM:
|
||||||
return PixelFormat::ABGR8U;
|
return PixelFormat::ABGR8U;
|
||||||
|
@ -616,11 +658,11 @@ struct SurfaceParams {
|
||||||
case Tegra::Texture::TextureFormat::Z24S8:
|
case Tegra::Texture::TextureFormat::Z24S8:
|
||||||
return PixelFormat::Z24S8;
|
return PixelFormat::Z24S8;
|
||||||
case Tegra::Texture::TextureFormat::DXT1:
|
case Tegra::Texture::TextureFormat::DXT1:
|
||||||
return PixelFormat::DXT1;
|
return is_srgb ? PixelFormat::DXT1_SRGB : PixelFormat::DXT1;
|
||||||
case Tegra::Texture::TextureFormat::DXT23:
|
case Tegra::Texture::TextureFormat::DXT23:
|
||||||
return PixelFormat::DXT23;
|
return is_srgb ? PixelFormat::DXT23_SRGB : PixelFormat::DXT23;
|
||||||
case Tegra::Texture::TextureFormat::DXT45:
|
case Tegra::Texture::TextureFormat::DXT45:
|
||||||
return PixelFormat::DXT45;
|
return is_srgb ? PixelFormat::DXT45_SRGB : PixelFormat::DXT45;
|
||||||
case Tegra::Texture::TextureFormat::DXN1:
|
case Tegra::Texture::TextureFormat::DXN1:
|
||||||
return PixelFormat::DXN1;
|
return PixelFormat::DXN1;
|
||||||
case Tegra::Texture::TextureFormat::DXN2:
|
case Tegra::Texture::TextureFormat::DXN2:
|
||||||
|
@ -634,19 +676,19 @@ struct SurfaceParams {
|
||||||
static_cast<u32>(component_type));
|
static_cast<u32>(component_type));
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
case Tegra::Texture::TextureFormat::BC7U:
|
case Tegra::Texture::TextureFormat::BC7U:
|
||||||
return PixelFormat::BC7U;
|
return is_srgb ? PixelFormat::BC7U_SRGB : PixelFormat::BC7U;
|
||||||
case Tegra::Texture::TextureFormat::BC6H_UF16:
|
case Tegra::Texture::TextureFormat::BC6H_UF16:
|
||||||
return PixelFormat::BC6H_UF16;
|
return PixelFormat::BC6H_UF16;
|
||||||
case Tegra::Texture::TextureFormat::BC6H_SF16:
|
case Tegra::Texture::TextureFormat::BC6H_SF16:
|
||||||
return PixelFormat::BC6H_SF16;
|
return PixelFormat::BC6H_SF16;
|
||||||
case Tegra::Texture::TextureFormat::ASTC_2D_4X4:
|
case Tegra::Texture::TextureFormat::ASTC_2D_4X4:
|
||||||
return PixelFormat::ASTC_2D_4X4;
|
return is_srgb ? PixelFormat::ASTC_2D_4X4_SRGB : PixelFormat::ASTC_2D_4X4;
|
||||||
case Tegra::Texture::TextureFormat::ASTC_2D_5X4:
|
case Tegra::Texture::TextureFormat::ASTC_2D_5X4:
|
||||||
return PixelFormat::ASTC_2D_5X4;
|
return is_srgb ? PixelFormat::ASTC_2D_5X4_SRGB : PixelFormat::ASTC_2D_5X4;
|
||||||
case Tegra::Texture::TextureFormat::ASTC_2D_8X8:
|
case Tegra::Texture::TextureFormat::ASTC_2D_8X8:
|
||||||
return PixelFormat::ASTC_2D_8X8;
|
return is_srgb ? PixelFormat::ASTC_2D_8X8_SRGB : PixelFormat::ASTC_2D_8X8;
|
||||||
case Tegra::Texture::TextureFormat::ASTC_2D_8X5:
|
case Tegra::Texture::TextureFormat::ASTC_2D_8X5:
|
||||||
return PixelFormat::ASTC_2D_8X5;
|
return is_srgb ? PixelFormat::ASTC_2D_8X5_SRGB : PixelFormat::ASTC_2D_8X5;
|
||||||
case Tegra::Texture::TextureFormat::R16_G16:
|
case Tegra::Texture::TextureFormat::R16_G16:
|
||||||
switch (component_type) {
|
switch (component_type) {
|
||||||
case Tegra::Texture::ComponentType::FLOAT:
|
case Tegra::Texture::ComponentType::FLOAT:
|
||||||
|
@ -881,7 +923,7 @@ struct SurfaceParams {
|
||||||
SurfaceTarget target;
|
SurfaceTarget target;
|
||||||
u32 max_mip_level;
|
u32 max_mip_level;
|
||||||
bool is_layered;
|
bool is_layered;
|
||||||
|
bool srgb_conversion;
|
||||||
// Parameters used for caching
|
// Parameters used for caching
|
||||||
VAddr addr;
|
VAddr addr;
|
||||||
Tegra::GPUVAddr gpu_addr;
|
Tegra::GPUVAddr gpu_addr;
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
OpenGLState OpenGLState::cur_state;
|
OpenGLState OpenGLState::cur_state;
|
||||||
|
bool OpenGLState::s_rgb_used;
|
||||||
OpenGLState::OpenGLState() {
|
OpenGLState::OpenGLState() {
|
||||||
// These all match default OpenGL values
|
// These all match default OpenGL values
|
||||||
|
framebuffer_srgb.enabled = false;
|
||||||
cull.enabled = false;
|
cull.enabled = false;
|
||||||
cull.mode = GL_BACK;
|
cull.mode = GL_BACK;
|
||||||
cull.front_face = GL_CCW;
|
cull.front_face = GL_CCW;
|
||||||
|
@ -89,6 +90,16 @@ OpenGLState::OpenGLState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLState::Apply() const {
|
void OpenGLState::Apply() const {
|
||||||
|
// sRGB
|
||||||
|
if (framebuffer_srgb.enabled != cur_state.framebuffer_srgb.enabled) {
|
||||||
|
if (framebuffer_srgb.enabled) {
|
||||||
|
// Track if sRGB is used
|
||||||
|
s_rgb_used = true;
|
||||||
|
glEnable(GL_FRAMEBUFFER_SRGB);
|
||||||
|
} else {
|
||||||
|
glDisable(GL_FRAMEBUFFER_SRGB);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Culling
|
// Culling
|
||||||
if (cull.enabled != cur_state.cull.enabled) {
|
if (cull.enabled != cur_state.cull.enabled) {
|
||||||
if (cull.enabled) {
|
if (cull.enabled) {
|
||||||
|
|
|
@ -35,6 +35,10 @@ constexpr TextureUnit ProcTexDiffLUT{9};
|
||||||
|
|
||||||
class OpenGLState {
|
class OpenGLState {
|
||||||
public:
|
public:
|
||||||
|
struct {
|
||||||
|
bool enabled; // GL_FRAMEBUFFER_SRGB
|
||||||
|
} framebuffer_srgb;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
bool enabled; // GL_CULL_FACE
|
bool enabled; // GL_CULL_FACE
|
||||||
GLenum mode; // GL_CULL_FACE_MODE
|
GLenum mode; // GL_CULL_FACE_MODE
|
||||||
|
@ -161,7 +165,12 @@ public:
|
||||||
static OpenGLState GetCurState() {
|
static OpenGLState GetCurState() {
|
||||||
return cur_state;
|
return cur_state;
|
||||||
}
|
}
|
||||||
|
static bool GetsRGBUsed() {
|
||||||
|
return s_rgb_used;
|
||||||
|
}
|
||||||
|
static void ClearsRGBUsed() {
|
||||||
|
s_rgb_used = false;
|
||||||
|
}
|
||||||
/// Apply this state as the current OpenGL state
|
/// Apply this state as the current OpenGL state
|
||||||
void Apply() const;
|
void Apply() const;
|
||||||
|
|
||||||
|
@ -176,6 +185,9 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static OpenGLState cur_state;
|
static OpenGLState cur_state;
|
||||||
|
// Workaround for sRGB problems caused by
|
||||||
|
// QT not supporting srgb output
|
||||||
|
static bool s_rgb_used;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -283,7 +283,8 @@ void RendererOpenGL::CreateRasterizer() {
|
||||||
if (rasterizer) {
|
if (rasterizer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Initialize sRGB Usage
|
||||||
|
OpenGLState::ClearsRGBUsed();
|
||||||
rasterizer = std::make_unique<RasterizerOpenGL>(render_window, screen_info);
|
rasterizer = std::make_unique<RasterizerOpenGL>(render_window, screen_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,13 +357,20 @@ void RendererOpenGL::DrawScreenTriangles(const ScreenInfo& screen_info, float x,
|
||||||
|
|
||||||
state.texture_units[0].texture = screen_info.display_texture;
|
state.texture_units[0].texture = screen_info.display_texture;
|
||||||
state.texture_units[0].swizzle = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA};
|
state.texture_units[0].swizzle = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA};
|
||||||
|
// Workaround brigthness problems in SMO by enabling sRGB in the final output
|
||||||
|
// if it has been used in the frame
|
||||||
|
// Needed because of this bug in QT
|
||||||
|
// QTBUG-50987
|
||||||
|
state.framebuffer_srgb.enabled = OpenGLState::GetsRGBUsed();
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices.data());
|
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices.data());
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
// restore default state
|
||||||
|
state.framebuffer_srgb.enabled = false;
|
||||||
state.texture_units[0].texture = 0;
|
state.texture_units[0].texture = 0;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
// Clear sRGB state for the next frame
|
||||||
|
OpenGLState::ClearsRGBUsed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -173,6 +173,7 @@ struct TICEntry {
|
||||||
};
|
};
|
||||||
union {
|
union {
|
||||||
BitField<0, 16, u32> width_minus_1;
|
BitField<0, 16, u32> width_minus_1;
|
||||||
|
BitField<22, 1, u32> srgb_conversion;
|
||||||
BitField<23, 4, TextureType> texture_type;
|
BitField<23, 4, TextureType> texture_type;
|
||||||
};
|
};
|
||||||
union {
|
union {
|
||||||
|
@ -227,6 +228,10 @@ struct TICEntry {
|
||||||
return header_version == TICHeaderVersion::BlockLinear ||
|
return header_version == TICHeaderVersion::BlockLinear ||
|
||||||
header_version == TICHeaderVersion::BlockLinearColorKey;
|
header_version == TICHeaderVersion::BlockLinearColorKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsSrgbConversionEnabled() const {
|
||||||
|
return srgb_conversion != 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
static_assert(sizeof(TICEntry) == 0x20, "TICEntry has wrong size");
|
static_assert(sizeof(TICEntry) == 0x20, "TICEntry has wrong size");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue