Commit b36a4816 by Jamie Madill Committed by Commit Bot

Vulkan: Add OpenGL line segment rasterization.

Line rasterization rules are implemented using a shader patch. The patch does a small test and discards pixels that are outside of the OpenGL line region. The feature is disabled on Android until we can determine the root cause of the test failures. Bug: angleproject:2598 Change-Id: Ic76c5e40fa3ceff7643e735e66f5a9050240c80b Reviewed-on: https://chromium-review.googlesource.com/1120153 Commit-Queue: Jamie Madill <jmadill@chromium.org> Reviewed-by: 's avatarShahbaz Youssefi <syoussefi@chromium.org>
parent beb669da
......@@ -591,6 +591,21 @@ bool IsTriangleMode(PrimitiveMode drawMode)
return false;
}
bool IsLineMode(PrimitiveMode primitiveMode)
{
switch (primitiveMode)
{
case PrimitiveMode::LineLoop:
case PrimitiveMode::LineStrip:
case PrimitiveMode::LineStripAdjacency:
case PrimitiveMode::Lines:
return true;
default:
return false;
}
}
bool IsIntegerFormat(GLenum unsizedFormat)
{
switch (unsizedFormat)
......
......@@ -72,6 +72,7 @@ IndexRange ComputeIndexRange(GLenum indexType,
GLuint GetPrimitiveRestartIndex(GLenum indexType);
bool IsTriangleMode(PrimitiveMode drawMode);
bool IsLineMode(PrimitiveMode primitiveMode);
bool IsIntegerFormat(GLenum unsizedFormat);
// Returns the product of the sizes in the vector, or 1 if the vector is empty. Doesn't currently
......
......@@ -863,6 +863,12 @@ void TIntermBlock::appendStatement(TIntermNode *statement)
}
}
void TIntermBlock::insertStatement(size_t insertPosition, TIntermNode *statement)
{
ASSERT(statement != nullptr);
mStatements.insert(mStatements.begin() + insertPosition, statement);
}
void TIntermDeclaration::appendDeclarator(TIntermTyped *declarator)
{
ASSERT(declarator != nullptr);
......
......@@ -239,7 +239,7 @@ class TIntermBranch : public TIntermNode
protected:
TOperator mFlowOp;
TIntermTyped *mExpression; // non-zero except for "return exp;" statements
TIntermTyped *mExpression; // zero except for "return exp;" statements
};
// Nodes that correspond to variable symbols in the source code. These may be regular variables or
......@@ -665,6 +665,7 @@ class TIntermBlock : public TIntermNode, public TIntermAggregateBase
// Only intended for initially building the block.
void appendStatement(TIntermNode *statement);
void insertStatement(size_t insertPosition, TIntermNode *statement);
TIntermSequence *getSequence() override { return &mStatements; }
const TIntermSequence *getSequence() const override { return &mStatements; }
......
......@@ -81,7 +81,6 @@ class TOutputGLSLBase : public TIntermTraverser
bool structDeclared(const TStructure *structure) const;
private:
void declareInterfaceBlockLayout(const TInterfaceBlock *interfaceBlock);
void declareInterfaceBlock(const TInterfaceBlock *interfaceBlock);
......
......@@ -44,6 +44,7 @@ class TSymbol : angle::NonCopyable
bool isFunction() const { return mSymbolClass == SymbolClass::Function; }
bool isVariable() const { return mSymbolClass == SymbolClass::Variable; }
bool isStruct() const { return mSymbolClass == SymbolClass::Struct; }
bool isInterfaceBlock() const { return mSymbolClass == SymbolClass::InterfaceBlock; }
const TSymbolUniqueId &uniqueId() const { return mUniqueId; }
SymbolType symbolType() const { return mSymbolType; }
......
......@@ -146,6 +146,8 @@ ContextVk::ContextVk(const gl::ContextState &state, RendererVk *renderer)
mDirtyBitHandlers[DIRTY_BIT_INDEX_BUFFER] = &ContextVk::handleDirtyIndexBuffer;
mDirtyBitHandlers[DIRTY_BIT_DRIVER_UNIFORMS] = &ContextVk::handleDirtyDriverUniforms;
mDirtyBitHandlers[DIRTY_BIT_DESCRIPTOR_SETS] = &ContextVk::handleDirtyDescriptorSets;
mDirtyBits = mNewCommandBufferDirtyBits;
}
ContextVk::~ContextVk() = default;
......
......@@ -288,7 +288,7 @@ class ContextVk : public ContextImpl, public vk::Context
float halfRenderAreaHeight;
float viewportYScale;
float invViewportYScale;
float negViewportYScale;
float padding;
// We'll use x, y, z for near / far / diff respectively.
......
......@@ -18,6 +18,7 @@
#include <array>
#include "common/FixedVector.h"
#include "common/string_utils.h"
#include "common/utilities.h"
#include "libANGLE/Caps.h"
......@@ -25,14 +26,17 @@
namespace rx
{
namespace
{
constexpr char kQualifierMarkerBegin[] = "@@ QUALIFIER-";
constexpr char kLayoutMarkerBegin[] = "@@ LAYOUT-";
constexpr char kMarkerEnd[] = " @@";
constexpr char kUniformQualifier[] = "uniform";
constexpr char kVersionDefine[] = "#version 450 core\n";
constexpr char kLineRasterDefine[] = R"(#version 450 core
#define ANGLE_ENABLE_LINE_SEGMENT_RASTERIZATION
)";
void GetBuiltInResourcesFromCaps(const gl::Caps &caps, TBuiltInResource *outBuiltInResources)
{
......@@ -203,15 +207,13 @@ void GlslangWrapper::GetShaderSource(const gl::ProgramState &programState,
// Bind the default uniforms for vertex and fragment shaders.
// See corresponding code in OutputVulkanGLSL.cpp.
std::stringstream searchStringBuilder;
searchStringBuilder << "@@ DEFAULT-UNIFORMS-SET-BINDING @@";
std::string searchString = searchStringBuilder.str();
std::string uniformsSearchString("@@ DEFAULT-UNIFORMS-SET-BINDING @@");
std::string vertexDefaultUniformsBinding = "set = 0, binding = 0";
std::string fragmentDefaultUniformsBinding = "set = 0, binding = 1";
angle::ReplaceSubstring(&vertexSource, searchString, vertexDefaultUniformsBinding);
angle::ReplaceSubstring(&fragmentSource, searchString, fragmentDefaultUniformsBinding);
angle::ReplaceSubstring(&vertexSource, uniformsSearchString, vertexDefaultUniformsBinding);
angle::ReplaceSubstring(&fragmentSource, uniformsSearchString, fragmentDefaultUniformsBinding);
// Assign textures to a descriptor set and binding.
int textureCount = 0;
......@@ -279,6 +281,18 @@ void GlslangWrapper::GetShaderSource(const gl::ProgramState &programState,
InsertQualifierSpecifierString(&vertexSource, kDriverBlockName, kUniformQualifier);
InsertQualifierSpecifierString(&fragmentSource, kDriverBlockName, kUniformQualifier);
// Substitute layout and qualifier strings for the position varying. Use the first free
// varying register after the packed varyings.
constexpr char kVaryingName[] = "ANGLEPosition";
std::stringstream layoutStream;
layoutStream << "location = " << (resources.varyingPacking.getMaxSemanticIndex() + 1);
const std::string layout = layoutStream.str();
InsertLayoutSpecifierString(&vertexSource, kVaryingName, layout);
InsertLayoutSpecifierString(&fragmentSource, kVaryingName, layout);
InsertQualifierSpecifierString(&vertexSource, kVaryingName, "out");
InsertQualifierSpecifierString(&fragmentSource, kVaryingName, "in");
*vertexSourceOut = vertexSource;
*fragmentSourceOut = fragmentSource;
}
......@@ -286,11 +300,45 @@ void GlslangWrapper::GetShaderSource(const gl::ProgramState &programState,
// static
angle::Result GlslangWrapper::GetShaderCode(vk::Context *context,
const gl::Caps &glCaps,
bool enableLineRasterEmulation,
const std::string &vertexSource,
const std::string &fragmentSource,
std::vector<uint32_t> *vertexCodeOut,
std::vector<uint32_t> *fragmentCodeOut)
{
if (enableLineRasterEmulation)
{
std::string patchedVertexSource = vertexSource;
std::string patchedFragmentSource = fragmentSource;
// #defines must come after the #version directive.
ANGLE_VK_CHECK(
context,
angle::ReplaceSubstring(&patchedVertexSource, kVersionDefine, kLineRasterDefine),
VK_ERROR_INVALID_SHADER_NV);
ANGLE_VK_CHECK(
context,
angle::ReplaceSubstring(&patchedFragmentSource, kVersionDefine, kLineRasterDefine),
VK_ERROR_INVALID_SHADER_NV);
return GetShaderCodeImpl(context, glCaps, patchedVertexSource, patchedFragmentSource,
vertexCodeOut, fragmentCodeOut);
}
else
{
return GetShaderCodeImpl(context, glCaps, vertexSource, fragmentSource, vertexCodeOut,
fragmentCodeOut);
}
}
// static
angle::Result GlslangWrapper::GetShaderCodeImpl(vk::Context *context,
const gl::Caps &glCaps,
const std::string &vertexSource,
const std::string &fragmentSource,
std::vector<uint32_t> *vertexCodeOut,
std::vector<uint32_t> *fragmentCodeOut)
{
std::array<const char *, 2> strings = {{vertexSource.c_str(), fragmentSource.c_str()}};
std::array<int, 2> lengths = {
{static_cast<int>(vertexSource.length()), static_cast<int>(fragmentSource.length())}};
......
......@@ -29,12 +29,20 @@ class GlslangWrapper
static angle::Result GetShaderCode(vk::Context *context,
const gl::Caps &glCaps,
bool enableLineRasterEmulation,
const std::string &vertexSource,
const std::string &fragmentSource,
std::vector<uint32_t> *vertexCodeOut,
std::vector<uint32_t> *fragmentCodeOut);
};
private:
static angle::Result GetShaderCodeImpl(vk::Context *context,
const gl::Caps &glCaps,
const std::string &vertexSource,
const std::string &fragmentSource,
std::vector<uint32_t> *vertexCodeOut,
std::vector<uint32_t> *fragmentCodeOut);
};
} // namespace rx
#endif // LIBANGLE_RENDERER_VULKAN_GLSLANG_WRAPPER_H_
......@@ -128,6 +128,12 @@ angle::Result SyncDefaultUniformBlock(ContextVk *contextVk,
ANGLE_TRY(dynamicBuffer->flush(contextVk));
return angle::Result::Continue();
}
bool UseLineRaster(const ContextVk *contextVk, const gl::DrawCallParams &drawCallParams)
{
return contextVk->getFeatures().basicGLLineRasterization &&
gl::IsLineMode(drawCallParams.mode());
}
} // anonymous namespace
// ProgramVk::ShaderInfo implementation.
......@@ -141,6 +147,7 @@ angle::Result ProgramVk::ShaderInfo::getShaders(
ContextVk *contextVk,
const std::string &vertexSource,
const std::string &fragmentSource,
bool enableLineRasterEmulation,
const vk::ShaderAndSerial **vertexShaderAndSerialOut,
const vk::ShaderAndSerial **fragmentShaderAndSerialOut)
{
......@@ -148,7 +155,8 @@ angle::Result ProgramVk::ShaderInfo::getShaders(
{
std::vector<uint32_t> vertexCode;
std::vector<uint32_t> fragmentCode;
ANGLE_TRY(GlslangWrapper::GetShaderCode(contextVk, contextVk->getCaps(), vertexSource,
ANGLE_TRY(GlslangWrapper::GetShaderCode(contextVk, contextVk->getCaps(),
enableLineRasterEmulation, vertexSource,
fragmentSource, &vertexCode, &fragmentCode));
ANGLE_TRY(vk::InitShaderAndSerial(contextVk, &mVertexShaderAndSerial, vertexCode.data(),
......@@ -211,8 +219,8 @@ angle::Result ProgramVk::reset(ContextVk *contextVk)
uniformBlock.storage.release(renderer);
}
// TODO(jmadill): Line rasterization emulation shaders. http://anglebug.com/2598
mDefaultShaderInfo.destroy(device);
mLineRasterShaderInfo.destroy(device);
Serial currentSerial = renderer->getCurrentQueueSerial();
renderer->releaseObject(currentSerial, &mEmptyUniformBlockStorage.memory);
......@@ -732,10 +740,20 @@ angle::Result ProgramVk::initShaders(ContextVk *contextVk,
const vk::ShaderAndSerial **fragmentShaderAndSerialOut,
const vk::PipelineLayout **pipelineLayoutOut)
{
// TODO(jmadill): Line rasterization emulation shaders. http://anglebug.com/2598
ANGLE_TRY(mDefaultShaderInfo.getShaders(contextVk, mVertexSource, mFragmentSource,
vertexShaderAndSerialOut, fragmentShaderAndSerialOut));
ASSERT(mDefaultShaderInfo.valid());
if (UseLineRaster(contextVk, drawCallParams))
{
ANGLE_TRY(mLineRasterShaderInfo.getShaders(contextVk, mVertexSource, mFragmentSource, true,
vertexShaderAndSerialOut,
fragmentShaderAndSerialOut));
ASSERT(mLineRasterShaderInfo.valid());
}
else
{
ANGLE_TRY(mDefaultShaderInfo.getShaders(contextVk, mVertexSource, mFragmentSource, false,
vertexShaderAndSerialOut,
fragmentShaderAndSerialOut));
ASSERT(mDefaultShaderInfo.valid());
}
*pipelineLayoutOut = &mPipelineLayout.get();
......@@ -925,7 +943,6 @@ angle::Result ProgramVk::updateDescriptorSets(ContextVk *contextVk,
const gl::DrawCallParams &drawCallParams,
vk::CommandBuffer *commandBuffer)
{
// TODO(jmadill): Line rasterization emulation shaders. http://anglebug.com/2598
// Can probably use better dirty bits here.
if (mUsedDescriptorSetRange.empty())
......
......@@ -183,6 +183,7 @@ class ProgramVk : public ProgramImpl
angle::Result getShaders(ContextVk *contextVk,
const std::string &vertexSource,
const std::string &fragmentSource,
bool enableLineRasterEmulation,
const vk::ShaderAndSerial **vertexShaderAndSerialOut,
const vk::ShaderAndSerial **fragmentShaderAndSerialOut);
void destroy(VkDevice device);
......@@ -193,8 +194,8 @@ class ProgramVk : public ProgramImpl
vk::ShaderAndSerial mFragmentShaderAndSerial;
};
// TODO(jmadill): Line rasterization emulation shaders. http://anglebug.com/2598
ShaderInfo mDefaultShaderInfo;
ShaderInfo mLineRasterShaderInfo;
// We keep the translated linked shader sources to use with shader draw call patching.
std::string mVertexSource;
......
......@@ -683,8 +683,13 @@ std::string RendererVk::getRendererDescription() const
void RendererVk::initFeatures()
{
// Use OpenGL line rasterization rules by default.
// Use OpenGL line rasterization rules by default.
// TODO(jmadill): Fix Android support. http://anglebug.com/2830
#if defined(ANGLE_PLATFORM_ANDROID)
mFeatures.basicGLLineRasterization = false;
#else
mFeatures.basicGLLineRasterization = true;
#endif // defined(ANGLE_PLATFORM_ANDROID)
// TODO(lucferron): Currently disabled on Intel only since many tests are failing and need
// investigation. http://anglebug.com/2728
......
......@@ -183,6 +183,19 @@
2630 GLES ANDROID : dEQP-GLES2.functional.shaders.struct.uniform.sampler_in_array_function_arg_* = FAIL
2630 GLES ANDROID : dEQP-GLES2.functional.shaders.struct.uniform.sampler_in_function_arg_* = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.depth.red_unsigned_byte = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.depth.rg_unsigned_byte = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.stencil.red_unsigned_byte = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.stencil.rg_unsigned_byte = FAIL
// Windows Linux and Mac failures
1028 WIN LINUX MAC : dEQP-GLES2.functional.fbo.completeness.renderable.texture.color0.srgb8 = FAIL
1028 WIN LINUX MAC : dEQP-GLES2.functional.fbo.completeness.renderable.texture.stencil.srgb8 = FAIL
1028 WIN LINUX MAC : dEQP-GLES2.functional.fbo.completeness.renderable.texture.depth.srgb8 = FAIL
// General Vulkan failures
// Nothing here!
// Android Vulkan backend only failures
2549 VULKAN ANDROID : dEQP-GLES2.functional.fragment_ops.depth_stencil.stencil* = SKIP
2606 VULKAN ANDROID : dEQP-GLES2.functional.debug_marker.random = SKIP
......@@ -190,23 +203,15 @@
2609 VULKAN ANDROID : dEQP-GLES2.functional.texture.mipmap.cube.generate.* = SKIP
2405 VULKAN ANDROID : dEQP-GLES2.functional.draw.random.42 = SKIP
2405 VULKAN ANDROID : dEQP-GLES2.functional.draw.random.59 = SKIP
2830 VULKAN ANDROID : dEQP-GLES2.functional.rasterization.primitives.line* = SKIP
// Fails on Nexus 5x only. TODO(jmadill): Remove suppression when possible. http://anglebug.com/2791
2791 VULKAN ANDROID : dEQP-GLES2.functional.clipping.* = SKIP
2599 VULKAN ANDROID : dEQP-GLES2.functional.rasterization.limits.points = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.depth.red_unsigned_byte = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.depth.rg_unsigned_byte = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.stencil.red_unsigned_byte = FAIL
2567 GLES ANDROID : dEQP-GLES2.functional.fbo.completeness.renderable.texture.stencil.rg_unsigned_byte = FAIL
// Windows Linux and Mac failures
1028 WIN LINUX MAC : dEQP-GLES2.functional.fbo.completeness.renderable.texture.color0.srgb8 = FAIL
1028 WIN LINUX MAC : dEQP-GLES2.functional.fbo.completeness.renderable.texture.stencil.srgb8 = FAIL
1028 WIN LINUX MAC : dEQP-GLES2.functional.fbo.completeness.renderable.texture.depth.srgb8 = FAIL
// Vulkan failures
2598 VULKAN : dEQP-GLES2.functional.rasterization.primitives.line* = SKIP
// Failing on the Pixel 2.
2727 VULKAN ANDROID : dEQP-GLES2.functional.shaders.builtin_variable.pointcoord = FAIL
2808 VULKAN ANDROID : dEQP-GLES2.functional.shaders.builtin_variable.fragcoord_w = FAIL
// Vulkan AMD Windows specific failures
2602 VULKAN WIN AMD : dEQP-GLES2.functional.buffer.write.* = SKIP
......@@ -243,6 +248,5 @@
2405 VULKAN WIN AMD : dEQP-GLES2.functional.vertex_arrays.single_attribute.usages.buffer_0_32_float* = SKIP
2405 VULKAN WIN AMD : dEQP-GLES2.functional.vertex_arrays.single_attribute.usages.buffer_0_32_short* = SKIP
// Failing on the Pixel 2.
2727 VULKAN ANDROID : dEQP-GLES2.functional.shaders.builtin_variable.pointcoord = FAIL
2808 VULKAN ANDROID : dEQP-GLES2.functional.shaders.builtin_variable.fragcoord_w = FAIL
// Fails after OpenGL line rasterization rules implementation. Possibly a bug in FragCoord.
2809 VULKAN WIN AMD : dEQP-GLES2.functional.clipping.line.long_line_clip = FAIL
......@@ -5,6 +5,7 @@
//
#include "test_utils/ANGLETest.h"
#include "test_utils/gl_raii.h"
using namespace angle;
......@@ -241,6 +242,86 @@ TEST_P(ViewportTest, TripleWindowOffCenter)
runScissoredTest();
}
// Test line rendering with a non-standard viewport.
TEST_P(ViewportTest, DrawLineWithViewport)
{
// We assume in the test the width and height are equal and we are tracing
// the line from bottom left to top right. Verify that all pixels along that line
// have been traced with green.
ASSERT_EQ(getWindowWidth(), getWindowHeight());
ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
glUseProgram(program);
std::vector<Vector3> vertices = {{-1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}};
const GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
ASSERT_NE(-1, positionLocation);
GLBuffer vertexBuffer;
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices[0]) * vertices.size(), vertices.data(),
GL_STATIC_DRAW);
glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(positionLocation);
// Set the viewport.
GLint quarterWidth = getWindowWidth() / 4;
GLint quarterHeight = getWindowHeight() / 4;
glViewport(quarterWidth, quarterHeight, quarterWidth, quarterHeight);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_LINES, 0, static_cast<GLsizei>(vertices.size()));
glDisableVertexAttribArray(positionLocation);
ASSERT_GL_NO_ERROR();
for (GLint x = quarterWidth; x < getWindowWidth() / 2; x++)
{
EXPECT_PIXEL_COLOR_EQ(x, x, GLColor::green);
}
}
// Test line rendering with an overly large viewport.
TEST_P(ViewportTest, DrawLineWithLargeViewport)
{
// We assume in the test the width and height are equal and we are tracing
// the line from bottom left to top right. Verify that all pixels along that line
// have been traced with green.
ASSERT_EQ(getWindowWidth(), getWindowHeight());
ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::Green());
glUseProgram(program);
std::vector<Vector3> vertices = {{-1.0f, -1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}};
const GLint positionLocation = glGetAttribLocation(program, essl1_shaders::PositionAttrib());
ASSERT_NE(-1, positionLocation);
GLBuffer vertexBuffer;
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices[0]) * vertices.size(), vertices.data(),
GL_STATIC_DRAW);
glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(positionLocation);
// Set the viewport.
glViewport(0, 0, getWindowWidth() * 2, getWindowHeight() * 2);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_LINES, 0, static_cast<GLsizei>(vertices.size()));
glDisableVertexAttribArray(positionLocation);
ASSERT_GL_NO_ERROR();
for (GLint x = 0; x < getWindowWidth(); x++)
{
EXPECT_PIXEL_COLOR_EQ(x, x, GLColor::green);
}
}
// Use this to select which configurations (e.g. which renderer, which GLES major version) these tests should be run against.
// D3D11 Feature Level 9 and D3D9 emulate large and negative viewports in the vertex shader. We should test both of these as well as D3D11 Feature Level 10_0+.
ANGLE_INSTANTIATE_TEST(ViewportTest,
......
......@@ -364,6 +364,17 @@ void main()
})";
}
// A shader that fills with 100% opaque green.
const char *Green()
{
return R"(precision mediump float;
void main()
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
})";
}
// A shader that fills with 100% opaque blue.
const char *Blue()
{
......
......@@ -83,6 +83,9 @@ ANGLE_EXPORT const char *UniformColor();
// A shader that fills with 100% opaque red.
ANGLE_EXPORT const char *Red();
// A shader that fills with 100% opaque green.
ANGLE_EXPORT const char *Green();
// A shader that fills with 100% opaque blue.
ANGLE_EXPORT const char *Blue();
......
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