Commit 55c25d0c by Jamie Madill

D3D11: Fix varying packing with structs.

Previously we would try to pass an entire struct through HLSL's shader interface. Instead, split this off as if each field was its own variable, which seems to be spec compliant (see ESSL 3.10). In the future we may want to fix register packing to use specific components of float4/int4/uint4 HLSL registers. This could also fix the remaining bugs in the SM3 packing. TEST=dEQP-GLES3.functional.shaders.linkage.varying.* BUG=angleproject:910 BUG=angleproject:1202 Change-Id: I1fd8b4505abc39bd2385ed5c088c316d55d0bc2c Reviewed-on: https://chromium-review.googlesource.com/311242Reviewed-by: 's avatarGeoff Lang <geofflang@chromium.org> Reviewed-by: 's avatarCorentin Wallez <cwallez@chromium.org> Tested-by: 's avatarJamie Madill <jmadill@chromium.org>
parent 9fc3682c
...@@ -51,7 +51,7 @@ const std::vector<VarT> &GetShaderVariables(const std::vector<VarT> *variableLis ...@@ -51,7 +51,7 @@ const std::vector<VarT> &GetShaderVariables(const std::vector<VarT> *variableLis
} // anonymous namespace } // anonymous namespace
// true if varying x has a higher priority in packing than y // true if varying x has a higher priority in packing than y
bool CompareVarying(const sh::Varying &x, const sh::Varying &y) bool CompareShaderVar(const sh::ShaderVariable &x, const sh::ShaderVariable &y)
{ {
if (x.type == y.type) if (x.type == y.type)
{ {
...@@ -293,7 +293,7 @@ void Shader::compile(Compiler *compiler) ...@@ -293,7 +293,7 @@ void Shader::compile(Compiler *compiler)
ASSERT(mData.mShaderType == GL_FRAGMENT_SHADER); ASSERT(mData.mShaderType == GL_FRAGMENT_SHADER);
// TODO(jmadill): Figure out why we only sort in the FS, and if we need to. // TODO(jmadill): Figure out why we only sort in the FS, and if we need to.
std::sort(mData.mVaryings.begin(), mData.mVaryings.end(), CompareVarying); std::sort(mData.mVaryings.begin(), mData.mVaryings.end(), CompareShaderVar);
mData.mActiveOutputVariables = mData.mActiveOutputVariables =
GetActiveShaderVariables(ShGetOutputVariables(compilerHandle)); GetActiveShaderVariables(ShGetOutputVariables(compilerHandle));
} }
......
...@@ -137,7 +137,7 @@ class Shader : angle::NonCopyable ...@@ -137,7 +137,7 @@ class Shader : angle::NonCopyable
ResourceManager *mResourceManager; ResourceManager *mResourceManager;
}; };
bool CompareVarying(const sh::Varying &x, const sh::Varying &y); bool CompareShaderVar(const sh::ShaderVariable &x, const sh::ShaderVariable &y);
} }
#endif // LIBANGLE_SHADER_H_ #endif // LIBANGLE_SHADER_H_
...@@ -18,10 +18,6 @@ ...@@ -18,10 +18,6 @@
#include "libANGLE/renderer/d3d/ShaderD3D.h" #include "libANGLE/renderer/d3d/ShaderD3D.h"
#include "libANGLE/renderer/d3d/VaryingPacking.h" #include "libANGLE/renderer/d3d/VaryingPacking.h"
// For use with ArrayString, see angleutils.h
static_assert(GL_INVALID_INDEX == UINT_MAX,
"GL_INVALID_INDEX must be equal to the max unsigned int.");
using namespace gl; using namespace gl;
namespace rx namespace rx
...@@ -107,6 +103,20 @@ const PixelShaderOutputVariable *FindOutputAtLocation( ...@@ -107,6 +103,20 @@ const PixelShaderOutputVariable *FindOutputAtLocation(
return nullptr; return nullptr;
} }
void WriteArrayString(std::stringstream &strstr, unsigned int i)
{
static_assert(GL_INVALID_INDEX == UINT_MAX,
"GL_INVALID_INDEX must be equal to the max unsigned int.");
if (i == UINT_MAX)
{
return;
}
strstr << "[";
strstr << i;
strstr << "]";
}
const std::string VERTEX_ATTRIBUTE_STUB_STRING = "@@ VERTEX ATTRIBUTES @@"; const std::string VERTEX_ATTRIBUTE_STUB_STRING = "@@ VERTEX ATTRIBUTES @@";
const std::string PIXEL_OUTPUT_STUB_STRING = "@@ PIXEL OUTPUT @@"; const std::string PIXEL_OUTPUT_STUB_STRING = "@@ PIXEL OUTPUT @@";
} // anonymous namespace } // anonymous namespace
...@@ -132,9 +142,8 @@ void DynamicHLSL::generateVaryingHLSL(const VaryingPacking &varyingPacking, ...@@ -132,9 +142,8 @@ void DynamicHLSL::generateVaryingHLSL(const VaryingPacking &varyingPacking,
for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList()) for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList())
{ {
const sh::Varying &varying = *registerInfo.packedVarying->varying; const auto &varying = *registerInfo.packedVarying->varying;
GLenum transposedType = gl::TransposeMatrixType(varying.type); ASSERT(!varying.isStruct());
unsigned int semanticIndex = registerInfo.semanticIndex;
// TODO: Add checks to ensure D3D interpolation modifiers don't result in too many // TODO: Add checks to ensure D3D interpolation modifiers don't result in too many
// registers being used. // registers being used.
...@@ -143,7 +152,7 @@ void DynamicHLSL::generateVaryingHLSL(const VaryingPacking &varyingPacking, ...@@ -143,7 +152,7 @@ void DynamicHLSL::generateVaryingHLSL(const VaryingPacking &varyingPacking,
// If the float varying has the 'nointerpolation' modifier on it then we would need // If the float varying has the 'nointerpolation' modifier on it then we would need
// N + 1 registers, and D3D compilation will fail. // N + 1 registers, and D3D compilation will fail.
switch (varying.interpolation) switch (registerInfo.packedVarying->interpolation)
{ {
case sh::INTERPOLATION_SMOOTH: case sh::INTERPOLATION_SMOOTH:
hlslStream << " "; hlslStream << " ";
...@@ -158,18 +167,11 @@ void DynamicHLSL::generateVaryingHLSL(const VaryingPacking &varyingPacking, ...@@ -158,18 +167,11 @@ void DynamicHLSL::generateVaryingHLSL(const VaryingPacking &varyingPacking,
UNREACHABLE(); UNREACHABLE();
} }
if (varying.isStruct()) GLenum transposedType = gl::TransposeMatrixType(varying.type);
{ GLenum componentType = gl::VariableComponentType(transposedType);
// TODO(jmadill): pass back translated name from the shader translator int columnCount = gl::VariableColumnCount(transposedType);
hlslStream << decorateVariable(varying.structName); hlslStream << HLSLComponentTypeString(componentType, columnCount);
} unsigned int semanticIndex = registerInfo.semanticIndex;
else
{
GLenum componentType = VariableComponentType(transposedType);
int columnCount = VariableColumnCount(transposedType);
hlslStream << HLSLComponentTypeString(componentType, columnCount);
}
hlslStream << " v" << semanticIndex << " : " << varyingSemantic << semanticIndex << ";\n"; hlslStream << " v" << semanticIndex << " : " << varyingSemantic << semanticIndex << ";\n";
} }
} }
...@@ -490,21 +492,27 @@ bool DynamicHLSL::generateShaderLinkHLSL(const gl::Data &data, ...@@ -490,21 +492,27 @@ bool DynamicHLSL::generateShaderLinkHLSL(const gl::Data &data,
for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList()) for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList())
{ {
const sh::Varying &varying = *registerInfo.packedVarying->varying; const auto &packedVarying = *registerInfo.packedVarying;
GLenum transposedType = TransposeMatrixType(varying.type); const auto &varying = *packedVarying.varying;
unsigned int variableRows = ASSERT(!varying.isStruct());
static_cast<unsigned int>(varying.isStruct() ? 1 : VariableRowCount(transposedType));
vertexStream << " output.v" << registerInfo.semanticIndex << " = ";
vertexStream << " output.v" << registerInfo.semanticIndex << " = _" + varying.name; if (packedVarying.isStructField())
{
vertexStream << decorateVariable(packedVarying.parentStructName) << ".";
}
vertexStream << decorateVariable(varying.name);
if (varying.isArray()) if (varying.isArray())
{ {
vertexStream << ArrayString(registerInfo.varyingArrayIndex); WriteArrayString(vertexStream, registerInfo.varyingArrayIndex);
} }
if (variableRows > 1) if (VariableRowCount(varying.type) > 1)
{ {
vertexStream << ArrayString(registerInfo.varyingRowIndex); WriteArrayString(vertexStream, registerInfo.varyingRowIndex);
} }
vertexStream << ";\n"; vertexStream << ";\n";
...@@ -623,47 +631,51 @@ bool DynamicHLSL::generateShaderLinkHLSL(const gl::Data &data, ...@@ -623,47 +631,51 @@ bool DynamicHLSL::generateShaderLinkHLSL(const gl::Data &data,
for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList()) for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList())
{ {
const sh::Varying &varying = *registerInfo.packedVarying->varying; const auto &packedVarying = *registerInfo.packedVarying;
const auto &varying = *packedVarying.varying;
ASSERT(!varying.isBuiltIn() && !varying.isStruct());
// Don't reference VS-only transform feedback varyings in the PS. // Don't reference VS-only transform feedback varyings in the PS.
if (registerInfo.packedVarying->vertexOnly) if (registerInfo.packedVarying->vertexOnly)
continue; continue;
ASSERT(!varying.isBuiltIn()); pixelStream << " ";
GLenum transposedType = TransposeMatrixType(varying.type);
int variableRows = (varying.isStruct() ? 1 : VariableRowCount(transposedType)); if (packedVarying.isStructField())
pixelStream << " _" << varying.name; {
pixelStream << decorateVariable(packedVarying.parentStructName) << ".";
}
pixelStream << decorateVariable(varying.name);
if (varying.isArray()) if (varying.isArray())
{ {
pixelStream << ArrayString(registerInfo.varyingArrayIndex); WriteArrayString(pixelStream, registerInfo.varyingArrayIndex);
} }
if (variableRows > 1) GLenum transposedType = TransposeMatrixType(varying.type);
if (VariableRowCount(transposedType) > 1)
{ {
pixelStream << ArrayString(registerInfo.varyingRowIndex); WriteArrayString(pixelStream, registerInfo.varyingRowIndex);
} }
pixelStream << " = input.v" << registerInfo.semanticIndex; pixelStream << " = input.v" << registerInfo.semanticIndex;
if (!varying.isStruct()) switch (VariableColumnCount(transposedType))
{ {
switch (VariableColumnCount(transposedType)) case 1:
{ pixelStream << ".x";
case 1: break;
pixelStream << ".x"; case 2:
break; pixelStream << ".xy";
case 2: break;
pixelStream << ".xy"; case 3:
break; pixelStream << ".xyz";
case 3: break;
pixelStream << ".xyz"; case 4:
break; break;
case 4: default:
break; UNREACHABLE();
default:
UNREACHABLE();
}
} }
pixelStream << ";\n"; pixelStream << ";\n";
} }
...@@ -712,10 +724,8 @@ std::string DynamicHLSL::generateGeometryShaderPreamble(const VaryingPacking &va ...@@ -712,10 +724,8 @@ std::string DynamicHLSL::generateGeometryShaderPreamble(const VaryingPacking &va
for (const PackedVaryingRegister &varyingRegister : varyingPacking.getRegisterList()) for (const PackedVaryingRegister &varyingRegister : varyingPacking.getRegisterList())
{ {
const sh::Varying &varying = *varyingRegister.packedVarying->varying;
preambleStream << " output.v" << varyingRegister.semanticIndex << " = "; preambleStream << " output.v" << varyingRegister.semanticIndex << " = ";
if (varying.interpolation == sh::INTERPOLATION_FLAT) if (varyingRegister.packedVarying->interpolation == sh::INTERPOLATION_FLAT)
{ {
preambleStream << "flat"; preambleStream << "flat";
} }
......
...@@ -104,7 +104,7 @@ struct AttributeSorter ...@@ -104,7 +104,7 @@ struct AttributeSorter
// true if varying x has a higher priority in packing than y // true if varying x has a higher priority in packing than y
bool ComparePackedVarying(const PackedVarying &x, const PackedVarying &y) bool ComparePackedVarying(const PackedVarying &x, const PackedVarying &y)
{ {
return gl::CompareVarying(*x.varying, *y.varying); return gl::CompareShaderVar(*x.varying, *y.varying);
} }
std::vector<PackedVarying> MergeVaryings(const gl::Shader &vertexShader, std::vector<PackedVarying> MergeVaryings(const gl::Shader &vertexShader,
...@@ -127,7 +127,20 @@ std::vector<PackedVarying> MergeVaryings(const gl::Shader &vertexShader, ...@@ -127,7 +127,20 @@ std::vector<PackedVarying> MergeVaryings(const gl::Shader &vertexShader,
{ {
if (output.name == input.name) if (output.name == input.name)
{ {
packedVaryings.push_back(PackedVarying(input)); if (output.isStruct())
{
ASSERT(!output.isArray());
for (const auto &field : output.fields)
{
ASSERT(!field.isStruct() && !field.isArray());
packedVaryings.push_back(
PackedVarying(field, input.interpolation, input.name));
}
}
else
{
packedVaryings.push_back(PackedVarying(input, input.interpolation));
}
packed = true; packed = true;
break; break;
} }
...@@ -140,8 +153,14 @@ std::vector<PackedVarying> MergeVaryings(const gl::Shader &vertexShader, ...@@ -140,8 +153,14 @@ std::vector<PackedVarying> MergeVaryings(const gl::Shader &vertexShader,
{ {
if (tfVarying == output.name) if (tfVarying == output.name)
{ {
packedVaryings.push_back(PackedVarying(output)); // Transform feedback for varying structs is underspecified.
packedVaryings.back().vertexOnly = true; // See Khronos bug 9856.
// TODO(jmadill): Figure out how to be spec-compliant here.
if (!output.isStruct())
{
packedVaryings.push_back(PackedVarying(output, output.interpolation));
packedVaryings.back().vertexOnly = true;
}
break; break;
} }
} }
...@@ -1391,9 +1410,10 @@ LinkResult ProgramD3D::link(const gl::Data &data, gl::InfoLog &infoLog) ...@@ -1391,9 +1410,10 @@ LinkResult ProgramD3D::link(const gl::Data &data, gl::InfoLog &infoLog)
mUsesFragDepth = metadata.usesFragDepth(mData); mUsesFragDepth = metadata.usesFragDepth(mData);
// Cache if we use flat shading // Cache if we use flat shading
mUsesFlatInterpolation = false;
for (const auto &varying : packedVaryings) for (const auto &varying : packedVaryings)
{ {
if (varying.varying->interpolation == sh::INTERPOLATION_FLAT) if (varying.interpolation == sh::INTERPOLATION_FLAT)
{ {
mUsesFlatInterpolation = true; mUsesFlatInterpolation = true;
break; break;
...@@ -2251,11 +2271,17 @@ void ProgramD3D::gatherTransformFeedbackVaryings(const VaryingPacking &varyingPa ...@@ -2251,11 +2271,17 @@ void ProgramD3D::gatherTransformFeedbackVaryings(const VaryingPacking &varyingPa
{ {
for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList()) for (const PackedVaryingRegister &registerInfo : varyingPacking.getRegisterList())
{ {
const sh::Varying &varying = *registerInfo.packedVarying->varying; const auto &varying = *registerInfo.packedVarying->varying;
GLenum transposedType = gl::TransposeMatrixType(varying.type); GLenum transposedType = gl::TransposeMatrixType(varying.type);
int componentCount = gl::VariableColumnCount(transposedType); int componentCount = gl::VariableColumnCount(transposedType);
ASSERT(!varying.isBuiltIn()); ASSERT(!varying.isBuiltIn());
// Transform feedback for varying structs is underspecified.
// See Khronos bug 9856.
// TODO(jmadill): Figure out how to be spec-compliant here.
if (registerInfo.packedVarying->isStructField() || varying.isStruct())
continue;
// There can be more than one register assigned to a particular varying, and each // There can be more than one register assigned to a particular varying, and each
// register needs its own stream out entry. // register needs its own stream out entry.
if (tfVaryingName == varying.name) if (tfVaryingName == varying.name)
......
...@@ -57,23 +57,16 @@ bool VaryingPacking::packVarying(const PackedVarying &packedVarying) ...@@ -57,23 +57,16 @@ bool VaryingPacking::packVarying(const PackedVarying &packedVarying)
unsigned int varyingRows = 0; unsigned int varyingRows = 0;
unsigned int varyingColumns = 0; unsigned int varyingColumns = 0;
const sh::Varying &varying = *packedVarying.varying; const auto &varying = *packedVarying.varying;
if (varying.isStruct()) // "Non - square matrices of type matCxR consume the same space as a square matrix of type matN
{ // where N is the greater of C and R.Variables of type mat2 occupies 2 complete rows."
varyingRows = HLSLVariableRegisterCount(varying, true); // Here we are a bit more conservative and allow packing non-square matrices more tightly.
varyingColumns = 4; // Make sure we use transposed matrix types to count registers correctly.
} ASSERT(!varying.isStruct());
else GLenum transposedType = gl::TransposeMatrixType(varying.type);
{ varyingRows = gl::VariableRowCount(transposedType);
// "Non - square matrices of type matCxR consume the same space as a square matrix of type varyingColumns = gl::VariableColumnCount(transposedType);
// matN where N is the greater of C and R.Variables of type mat2 occupies 2 complete rows."
// Here we are a bit more conservative and allow packing non-square matrices more tightly.
// Make sure we use transposed matrix types to count registers correctly.
GLenum transposedType = gl::TransposeMatrixType(varying.type);
varyingRows = gl::VariableRowCount(transposedType);
varyingColumns = gl::VariableColumnCount(transposedType);
}
// "Arrays of size N are assumed to take N times the size of the base type" // "Arrays of size N are assumed to take N times the size of the base type"
varyingRows *= varying.elementCount(); varyingRows *= varying.elementCount();
...@@ -206,18 +199,11 @@ void VaryingPacking::insert(unsigned int registerRow, ...@@ -206,18 +199,11 @@ void VaryingPacking::insert(unsigned int registerRow,
unsigned int varyingRows = 0; unsigned int varyingRows = 0;
unsigned int varyingColumns = 0; unsigned int varyingColumns = 0;
const sh::Varying &varying = *packedVarying.varying; const auto &varying = *packedVarying.varying;
if (varying.isStruct()) ASSERT(!varying.isStruct());
{ GLenum transposedType = gl::TransposeMatrixType(varying.type);
varyingRows = HLSLVariableRegisterCount(varying, true); varyingRows = gl::VariableRowCount(transposedType);
varyingColumns = 4; varyingColumns = gl::VariableColumnCount(transposedType);
}
else
{
GLenum transposedType = gl::TransposeMatrixType(varying.type);
varyingRows = gl::VariableRowCount(transposedType);
varyingColumns = gl::VariableColumnCount(transposedType);
}
PackedVaryingRegister registerInfo; PackedVaryingRegister registerInfo;
registerInfo.packedVarying = &packedVarying; registerInfo.packedVarying = &packedVarying;
...@@ -251,14 +237,15 @@ bool VaryingPacking::packVaryings(gl::InfoLog &infoLog, ...@@ -251,14 +237,15 @@ bool VaryingPacking::packVaryings(gl::InfoLog &infoLog,
// subrectangle. No splitting of variables is permitted." // subrectangle. No splitting of variables is permitted."
for (const PackedVarying &packedVarying : packedVaryings) for (const PackedVarying &packedVarying : packedVaryings)
{ {
const sh::Varying &varying = *packedVarying.varying; const auto &varying = *packedVarying.varying;
// Do not assign registers to built-in or unreferenced varyings // Do not assign registers to built-in or unreferenced varyings
if (varying.isBuiltIn() || !varying.staticUse) if (varying.isBuiltIn() || (!varying.staticUse && !packedVarying.isStructField()))
{ {
continue; continue;
} }
ASSERT(!varying.isStruct());
ASSERT(uniqueVaryingNames.count(varying.name) == 0); ASSERT(uniqueVaryingNames.count(varying.name) == 0);
if (packVarying(packedVarying)) if (packVarying(packedVarying))
...@@ -282,7 +269,7 @@ bool VaryingPacking::packVaryings(gl::InfoLog &infoLog, ...@@ -282,7 +269,7 @@ bool VaryingPacking::packVaryings(gl::InfoLog &infoLog,
for (const PackedVarying &packedVarying : packedVaryings) for (const PackedVarying &packedVarying : packedVaryings)
{ {
const sh::Varying &varying = *packedVarying.varying; const auto &varying = *packedVarying.varying;
// Make sure transform feedback varyings aren't optimized out. // Make sure transform feedback varyings aren't optimized out.
if (uniqueVaryingNames.count(transformFeedbackVaryingName) == 0) if (uniqueVaryingNames.count(transformFeedbackVaryingName) == 0)
......
...@@ -19,12 +19,32 @@ class ProgramD3DMetadata; ...@@ -19,12 +19,32 @@ class ProgramD3DMetadata;
struct PackedVarying struct PackedVarying
{ {
PackedVarying(const sh::Varying &varyingIn) : varying(&varyingIn), vertexOnly(false) {} PackedVarying(const sh::ShaderVariable &varyingIn, sh::InterpolationType interpolationIn)
: varying(&varyingIn), vertexOnly(false), interpolation(interpolationIn)
{
}
PackedVarying(const sh::ShaderVariable &varyingIn,
sh::InterpolationType interpolationIn,
const std::string &parentStructNameIn)
: varying(&varyingIn),
vertexOnly(false),
interpolation(interpolationIn),
parentStructName(parentStructNameIn)
{
}
bool isStructField() const { return !parentStructName.empty(); }
const sh::Varying *varying; const sh::ShaderVariable *varying;
// Transform feedback varyings can be only referenced in the VS. // Transform feedback varyings can be only referenced in the VS.
bool vertexOnly; bool vertexOnly;
// Cached so we can store sh::ShaderVariable to point to varying fields.
sh::InterpolationType interpolation;
// Struct name
std::string parentStructName;
}; };
struct PackedVaryingRegister final struct PackedVaryingRegister final
...@@ -52,6 +72,8 @@ struct PackedVaryingRegister final ...@@ -52,6 +72,8 @@ struct PackedVaryingRegister final
return registerRow * 4 + registerColumn; return registerRow * 4 + registerColumn;
} }
bool isStructField() const { return !structFieldName.empty(); }
// Index to the array of varyings. // Index to the array of varyings.
const PackedVarying *packedVarying; const PackedVarying *packedVarying;
...@@ -69,6 +91,9 @@ struct PackedVaryingRegister final ...@@ -69,6 +91,9 @@ struct PackedVaryingRegister final
// Assigned after packing // Assigned after packing
unsigned int semanticIndex; unsigned int semanticIndex;
// Struct member this varying corresponds to.
std::string structFieldName;
}; };
class VaryingPacking final : angle::NonCopyable class VaryingPacking final : angle::NonCopyable
......
...@@ -40,8 +40,6 @@ ...@@ -40,8 +40,6 @@
// TODO(jmadill): triage these into permanent and temporary failures // TODO(jmadill): triage these into permanent and temporary failures
1088 WIN : dEQP-GLES3.functional.shaders.linkage.varying.struct.float_vec3 = FAIL
1088 WIN : dEQP-GLES3.functional.shaders.linkage.varying.struct.float_uvec2_vec3 = FAIL
1089 WIN : dEQP-GLES3.functional.shaders.functions.invalid.local_function_prototype_vertex = FAIL 1089 WIN : dEQP-GLES3.functional.shaders.functions.invalid.local_function_prototype_vertex = FAIL
1089 WIN : dEQP-GLES3.functional.shaders.functions.invalid.local_function_prototype_fragment = FAIL 1089 WIN : dEQP-GLES3.functional.shaders.functions.invalid.local_function_prototype_fragment = FAIL
1089 WIN : dEQP-GLES3.functional.shaders.functions.invalid.overload_builtin_function_vertex = FAIL 1089 WIN : dEQP-GLES3.functional.shaders.functions.invalid.overload_builtin_function_vertex = FAIL
......
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