Commit 049a187a by Alexis Hetu Committed by Alexis Hétu

SRGB implementation

The SRGB conversion code was already available, but wasn't used specifically for the SRGB type framebuffers. Also, the SRGB conversion should always be applied after blending. According to the GLES 3.0 spec, section 4.1.8 - sRGB Conversion: "the R, G, and B values after blending are converted into the non-linear sRGB color space by computing." All related dEQP tests pass. Change-Id: I9342d2f74aa650f28835a951bdfa8bd371bc6924 Reviewed-on: https://swiftshader-review.googlesource.com/5189Tested-by: 's avatarAlexis Hétu <sugoi@google.com> Reviewed-by: 's avatarNicolas Capens <capn@google.com> Tested-by: 's avatarNicolas Capens <capn@google.com>
parent 5c26c290
......@@ -369,6 +369,8 @@ namespace sw
break;
case FORMAT_X8B8G8R8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
{
Int x = x0;
......@@ -689,6 +691,8 @@ namespace sw
break;
case FORMAT_X8B8G8R8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
{
c1 = Swizzle(c1, 0xC6);
......
......@@ -499,6 +499,7 @@ GLenum Framebuffer::getImplementationColorReadFormat()
case sw::FORMAT_A32B32G32R32F:
case sw::FORMAT_A8R8G8B8:
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_A1R5G5B5: return GL_BGRA_EXT;
case sw::FORMAT_X8B8G8R8I:
case sw::FORMAT_X8B8G8R8UI:
......@@ -509,6 +510,7 @@ GLenum Framebuffer::getImplementationColorReadFormat()
case sw::FORMAT_X32B32G32R32F:
case sw::FORMAT_B16G16R16F:
case sw::FORMAT_X8B8G8R8I_SNORM:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_X8B8G8R8: return GL_RGB;
case sw::FORMAT_X8R8G8B8: return 0x80E0; // GL_BGR_EXT
case sw::FORMAT_R5G6B5: return GL_RGB;
......@@ -563,6 +565,8 @@ GLenum Framebuffer::getImplementationColorReadType()
case sw::FORMAT_A8B8G8R8I_SNORM:return GL_BYTE;
case sw::FORMAT_R8:
case sw::FORMAT_G8R8:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_A8R8G8B8:
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_X8R8G8B8:
......
......@@ -658,6 +658,7 @@ void Texture2D::bindTexImage(egl::Surface *surface)
switch(surface->getInternalFormat())
{
case sw::FORMAT_A8R8G8B8:
case sw::FORMAT_SRGB8_A8:
format = GL_BGRA_EXT;
break;
case sw::FORMAT_A8B8G8R8:
......@@ -665,7 +666,8 @@ void Texture2D::bindTexImage(egl::Surface *surface)
break;
case sw::FORMAT_X8B8G8R8:
case sw::FORMAT_X8R8G8B8:
format = GL_RGB;
case sw::FORMAT_SRGB8_X8:
format = GL_RGB;
break;
default:
UNIMPLEMENTED();
......
......@@ -1520,7 +1520,8 @@ namespace es2sw
case GL_RGB32F: return sw::FORMAT_B32G32R32F;
case GL_RGBA32F: return sw::FORMAT_A32B32G32R32F;
case GL_RGB10_A2: return sw::FORMAT_A2B10G10R10;
case GL_SRGB8_ALPHA8: // FIXME: Implement SRGB
case GL_SRGB8: return sw::FORMAT_SRGB8_X8;
case GL_SRGB8_ALPHA8: return sw::FORMAT_SRGB8_A8;
default: UNREACHABLE(format); return sw::FORMAT_NULL;
}
}
......@@ -1571,6 +1572,7 @@ namespace sw2es
return 2;
case sw::FORMAT_A8R8G8B8:
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_A8B8G8R8I:
case sw::FORMAT_A8B8G8R8UI:
case sw::FORMAT_A8B8G8R8I_SNORM:
......@@ -1581,6 +1583,7 @@ namespace sw2es
return 1;
case sw::FORMAT_X8R8G8B8:
case sw::FORMAT_X8B8G8R8:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_R5G6B5:
return 0;
default:
......@@ -1626,6 +1629,8 @@ namespace sw2es
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_X8R8G8B8:
case sw::FORMAT_X8B8G8R8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_R8:
case sw::FORMAT_G8R8:
case sw::FORMAT_R8I:
......@@ -1681,6 +1686,8 @@ namespace sw2es
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_X8R8G8B8:
case sw::FORMAT_X8B8G8R8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_G8R8:
case sw::FORMAT_G8R8I:
case sw::FORMAT_X8B8G8R8I:
......@@ -1727,6 +1734,8 @@ namespace sw2es
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_X8R8G8B8:
case sw::FORMAT_X8B8G8R8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_X8B8G8R8I:
case sw::FORMAT_A8B8G8R8I:
case sw::FORMAT_X8B8G8R8UI:
......@@ -1850,6 +1859,8 @@ namespace sw2es
case sw::FORMAT_A8B8G8R8:
case sw::FORMAT_X8R8G8B8:
case sw::FORMAT_X8B8G8R8:
case sw::FORMAT_SRGB8_A8:
case sw::FORMAT_SRGB8_X8:
case sw::FORMAT_A1R5G5B5:
case sw::FORMAT_R5G6B5:
return GL_UNSIGNED_NORMALIZED;
......@@ -1883,6 +1894,8 @@ namespace sw2es
case sw::FORMAT_R5G6B5: return GL_RGB565;
case sw::FORMAT_X8R8G8B8: return GL_RGB8_OES;
case sw::FORMAT_X8B8G8R8: return GL_RGB8_OES;
case sw::FORMAT_SRGB8_A8: return GL_RGBA8_OES;
case sw::FORMAT_SRGB8_X8: return GL_RGB8_OES;
default:
UNREACHABLE(format);
}
......
......@@ -178,6 +178,7 @@ namespace sw
break;
case FORMAT_A8B8G8R8:
case FORMAT_A8B8G8R8UI:
case FORMAT_SRGB8_A8:
c = Float4(*Pointer<Byte4>(element));
break;
case FORMAT_X8R8G8B8:
......@@ -203,6 +204,7 @@ namespace sw
break;
case FORMAT_X8B8G8R8:
case FORMAT_X8B8G8R8UI:
case FORMAT_SRGB8_X8:
c = Float4(*Pointer<Byte4>(element));
c.w = float(0xFF);
break;
......@@ -355,6 +357,7 @@ namespace sw
}
break;
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_A8:
if(writeRGBA)
{
UShort4 c0 = As<UShort4>(RoundShort4(c));
......@@ -385,6 +388,7 @@ namespace sw
}
break;
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
if(writeRGBA)
{
UShort4 c0 = As<UShort4>(RoundShort4(c));
......@@ -924,6 +928,8 @@ namespace sw
case FORMAT_B8G8R8:
case FORMAT_X8B8G8R8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
scale = vector(0xFF, 0xFF, 0xFF, 0xFF);
break;
case FORMAT_R8I_SNORM:
......
......@@ -130,6 +130,7 @@ namespace sw
(static_cast<unsigned int>(snorm<8>(color.r)) << 0);
break;
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_A8:
*(unsigned int*)element = (unorm<8>(color.a) << 24) | (unorm<8>(color.b) << 16) | (unorm<8>(color.g) << 8) | (unorm<8>(color.r) << 0);
break;
case FORMAT_A8B8G8R8I:
......@@ -148,6 +149,7 @@ namespace sw
(static_cast<unsigned int>(snorm<8>(color.r)) << 0);
break;
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
*(unsigned int*)element = 0xFF000000 | (unorm<8>(color.b) << 16) | (unorm<8>(color.g) << 8) | (unorm<8>(color.r) << 0);
break;
case FORMAT_X8B8G8R8I:
......@@ -542,6 +544,7 @@ namespace sw
}
break;
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_A8:
{
unsigned int abgr = *(unsigned int*)element;
......@@ -581,6 +584,7 @@ namespace sw
}
break;
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
{
unsigned int xbgr = *(unsigned int*)element;
......@@ -1469,6 +1473,8 @@ namespace sw
// case FORMAT_A8G8R8B8Q: return 4;
case FORMAT_X8B8G8R8I: return 4;
case FORMAT_X8B8G8R8: return 4;
case FORMAT_SRGB8_X8: return 4;
case FORMAT_SRGB8_A8: return 4;
case FORMAT_A8B8G8R8I: return 4;
case FORMAT_R8UI: return 1;
case FORMAT_G8R8UI: return 2;
......@@ -2645,6 +2651,8 @@ namespace sw
case FORMAT_X8B8G8R8I:
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_A8B8G8R8I:
case FORMAT_R8UI:
case FORMAT_G8R8UI:
......@@ -2725,6 +2733,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_G8R8:
case FORMAT_A2B10G10R10:
case FORMAT_R16UI:
......@@ -2804,6 +2814,8 @@ namespace sw
case FORMAT_X8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_R5G6B5:
case FORMAT_X1R5G5B5:
case FORMAT_A1R5G5B5:
......@@ -2833,6 +2845,8 @@ namespace sw
case FORMAT_X8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_R5G6B5:
return true;
default:
......@@ -2939,6 +2953,8 @@ namespace sw
case FORMAT_X8B8G8R8I: return 3;
case FORMAT_X8B8G8R8: return 3;
case FORMAT_A8R8G8B8: return 4;
case FORMAT_SRGB8_X8: return 3;
case FORMAT_SRGB8_A8: return 4;
case FORMAT_A8B8G8R8I: return 4;
case FORMAT_A8B8G8R8: return 4;
case FORMAT_G8R8I: return 2;
......@@ -3531,6 +3547,10 @@ namespace sw
case FORMAT_B8G8R8:
case FORMAT_X8B8G8R8:
return FORMAT_X8B8G8R8;
case FORMAT_SRGB8_X8:
return FORMAT_SRGB8_X8;
case FORMAT_SRGB8_A8:
return FORMAT_SRGB8_A8;
// Compressed formats
#if S3TC_SUPPORT
case FORMAT_DXT1:
......@@ -3687,7 +3707,9 @@ namespace sw
unsigned char *sourceE = sourceD + slice;
unsigned char *sourceF = sourceE + slice;
if(internal.format == FORMAT_X8R8G8B8 || internal.format == FORMAT_A8R8G8B8 || internal.format == FORMAT_X8B8G8R8 || internal.format == FORMAT_A8B8G8R8)
if(internal.format == FORMAT_X8R8G8B8 || internal.format == FORMAT_A8R8G8B8 ||
internal.format == FORMAT_X8B8G8R8 || internal.format == FORMAT_A8B8G8R8 ||
internal.format == FORMAT_SRGB8_X8 || internal.format == FORMAT_SRGB8_A8)
{
if(CPUID::supportsSSE2() && (width % 4) == 0)
{
......
......@@ -76,6 +76,8 @@ namespace sw
FORMAT_A8B8G8R8UI,
FORMAT_A8B8G8R8I_SNORM,
FORMAT_A8B8G8R8, // UI_SNORM
FORMAT_SRGB8_X8,
FORMAT_SRGB8_A8,
FORMAT_X1R5G5B5,
FORMAT_A1R5G5B5,
FORMAT_R5G5B5A1,
......
......@@ -578,7 +578,7 @@ namespace sw
continue;
}
if(!postBlendSRGB && state.writeSRGB)
if(!postBlendSRGB && state.writeSRGB && !isSRGB(index))
{
c[index].x = linearToSRGB(c[index].x);
c[index].y = linearToSRGB(c[index].y);
......@@ -597,6 +597,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_A8:
case FORMAT_G16R16:
case FORMAT_A16B16G16R16:
......@@ -733,6 +735,8 @@ namespace sw
case FORMAT_A8B8G8R8:
case FORMAT_X8R8G8B8:
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_A8:
case FORMAT_G16R16:
case FORMAT_A16B16G16R16:
......
......@@ -996,6 +996,11 @@ namespace sw
}
}
bool PixelRoutine::isSRGB(int index) const
{
return state.targetFormat[index] == FORMAT_SRGB8_A8 || state.targetFormat[index] == FORMAT_SRGB8_X8;
}
void PixelRoutine::readPixel(int index, Pointer<Byte> &cBuffer, Int &x, Vector4s &pixel)
{
Short4 c01;
......@@ -1035,6 +1040,7 @@ namespace sw
pixel.w = UnpackHigh(As<Byte8>(pixel.w), As<Byte8>(pixel.w));
break;
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_A8:
buffer = cBuffer + 4 * x;
c01 = *Pointer<Short4>(buffer);
buffer += *Pointer<Int>(data + OFFSET(DrawData, colorPitchB[index]));
......@@ -1082,6 +1088,7 @@ namespace sw
pixel.w = Short4(0xFFFFu);
break;
case FORMAT_X8B8G8R8:
case FORMAT_SRGB8_X8:
buffer = cBuffer + 4 * x;
c01 = *Pointer<Short4>(buffer);
buffer += *Pointer<Int>(data + OFFSET(DrawData, colorPitchB[index]));
......@@ -1141,7 +1148,7 @@ namespace sw
ASSERT(false);
}
if(postBlendSRGB && state.writeSRGB)
if((postBlendSRGB && state.writeSRGB) || isSRGB(index))
{
sRGBtoLinear16_12_16(pixel);
}
......@@ -1363,7 +1370,7 @@ namespace sw
void PixelRoutine::writeColor(int index, Pointer<Byte> &cBuffer, Int &x, Vector4s &current, Int &sMask, Int &zMask, Int &cMask)
{
if(postBlendSRGB && state.writeSRGB)
if((postBlendSRGB && state.writeSRGB) || isSRGB(index))
{
linearToSRGB16_12_16(current);
}
......@@ -1383,6 +1390,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
current.x = current.x - As<Short4>(As<UShort4>(current.x) >> 8) + Short4(0x0080, 0x0080, 0x0080, 0x0080);
current.y = current.y - As<Short4>(As<UShort4>(current.y) >> 8) + Short4(0x0080, 0x0080, 0x0080, 0x0080);
current.z = current.z - As<Short4>(As<UShort4>(current.z) >> 8) + Short4(0x0080, 0x0080, 0x0080, 0x0080);
......@@ -1465,7 +1474,9 @@ namespace sw
break;
case FORMAT_X8B8G8R8:
case FORMAT_A8B8G8R8:
if(state.targetFormat[index] == FORMAT_X8B8G8R8 || rgbaWriteMask == 0x7)
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
if(state.targetFormat[index] == FORMAT_X8B8G8R8 || state.targetFormat[index] == FORMAT_SRGB8_X8 || rgbaWriteMask == 0x7)
{
current.x = As<Short4>(As<UShort4>(current.x) >> 8);
current.y = As<Short4>(As<UShort4>(current.y) >> 8);
......@@ -1655,13 +1666,17 @@ namespace sw
break;
case FORMAT_A8B8G8R8:
case FORMAT_X8B8G8R8: // FIXME: Don't touch alpha?
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
{
Pointer<Byte> buffer = cBuffer + x * 4;
Short4 value = *Pointer<Short4>(buffer);
if((state.targetFormat[index] == FORMAT_A8B8G8R8 && rgbaWriteMask != 0x0000000F) ||
((state.targetFormat[index] == FORMAT_X8B8G8R8 && rgbaWriteMask != 0x00000007) &&
(state.targetFormat[index] == FORMAT_X8B8G8R8 && rgbaWriteMask != 0x0000000F))) // FIXME: Need for masking when XBGR && Fh?
bool masked = (((state.targetFormat[index] == FORMAT_A8B8G8R8 || state.targetFormat[index] == FORMAT_SRGB8_A8) && rgbaWriteMask != 0x0000000F) ||
(((state.targetFormat[index] == FORMAT_X8B8G8R8 || state.targetFormat[index] == FORMAT_SRGB8_X8) && rgbaWriteMask != 0x00000007) &&
((state.targetFormat[index] == FORMAT_X8B8G8R8 || state.targetFormat[index] == FORMAT_SRGB8_X8) && rgbaWriteMask != 0x0000000F))); // FIXME: Need for masking when XBGR && Fh?
if(masked)
{
Short4 masked = value;
c01 &= *Pointer<Short4>(constants + OFFSET(Constants,maskB4Q[rgbaWriteMask][0]));
......@@ -1677,9 +1692,7 @@ namespace sw
buffer += *Pointer<Int>(data + OFFSET(DrawData,colorPitchB[index]));
value = *Pointer<Short4>(buffer);
if((state.targetFormat[index] == FORMAT_A8B8G8R8 && rgbaWriteMask != 0x0000000F) ||
((state.targetFormat[index] == FORMAT_X8B8G8R8 && rgbaWriteMask != 0x00000007) &&
(state.targetFormat[index] == FORMAT_X8B8G8R8 && rgbaWriteMask != 0x0000000F))) // FIXME: Need for masking when XBGR && Fh?
if(masked)
{
Short4 masked = value;
c23 &= *Pointer<Short4>(constants + OFFSET(Constants,maskB4Q[rgbaWriteMask][0]));
......@@ -2026,7 +2039,7 @@ namespace sw
ASSERT(false);
}
if(postBlendSRGB && state.writeSRGB)
if((postBlendSRGB && state.writeSRGB) || isSRGB(index))
{
sRGBtoLinear(pixel.x);
sRGBtoLinear(pixel.y);
......
......@@ -58,6 +58,7 @@ namespace sw
void alphaBlend(int index, Pointer<Byte> &cBuffer, Vector4f &oC, Int &x);
void writeColor(int index, Pointer<Byte> &cBuffer, Int &i, Vector4f &oC, Int &sMask, Int &zMask, Int &cMask);
bool isSRGB(int index) const;
UShort4 convertFixed16(Float4 &cf, bool saturate = true);
void linearToSRGB12_16(Vector4s &c);
......
......@@ -222,6 +222,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_V8U8:
case FORMAT_Q8W8V8U8:
case FORMAT_X8L8V8U8:
......@@ -480,6 +482,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_V8U8:
case FORMAT_Q8W8V8U8:
case FORMAT_X8L8V8U8:
......@@ -1738,6 +1742,7 @@ namespace sw
case FORMAT_A8B8G8R8UI:
case FORMAT_A8B8G8R8I_SNORM:
case FORMAT_Q8W8V8U8:
case FORMAT_SRGB8_A8:
c.z = c.x;
c.x = As<Short4>(UnpackLow(c.x, c.y));
c.z = As<Short4>(UnpackHigh(c.z, c.y));
......@@ -1778,6 +1783,7 @@ namespace sw
case FORMAT_X8B8G8R8I:
case FORMAT_X8B8G8R8:
case FORMAT_X8L8V8U8:
case FORMAT_SRGB8_X8:
c.z = c.x;
c.x = As<Short4>(UnpackLow(c.x, c.y));
c.z = As<Short4>(UnpackHigh(c.z, c.y));
......@@ -2213,6 +2219,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_V8U8:
case FORMAT_Q8W8V8U8:
case FORMAT_X8L8V8U8:
......@@ -2262,6 +2270,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_V8U8:
case FORMAT_Q8W8V8U8:
case FORMAT_X8L8V8U8:
......@@ -2354,6 +2364,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_V8U8:
case FORMAT_Q8W8V8U8:
case FORMAT_X8L8V8U8:
......@@ -2429,6 +2441,8 @@ namespace sw
case FORMAT_X8B8G8R8:
case FORMAT_A8R8G8B8:
case FORMAT_A8B8G8R8:
case FORMAT_SRGB8_X8:
case FORMAT_SRGB8_A8:
case FORMAT_V8U8:
case FORMAT_Q8W8V8U8:
case FORMAT_X8L8V8U8:
......@@ -2496,6 +2510,8 @@ namespace sw
case FORMAT_X8B8G8R8: return component < 3;
case FORMAT_A8R8G8B8: return component < 3;
case FORMAT_A8B8G8R8: return component < 3;
case FORMAT_SRGB8_X8: return component < 3;
case FORMAT_SRGB8_A8: return component < 3;
case FORMAT_V8U8: return false;
case FORMAT_Q8W8V8U8: return false;
case FORMAT_X8L8V8U8: return false;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment