Commit 3026829e by Olli Etuaho Committed by Jamie Madill

Pass texture base level to shaders on D3D11

The base level is passed to shaders in a uniform block created specifically for passing sampler metadata. This is done on feature levels above 9_3, which treat samplers as indices to sampler arrays in shaders. BUG=angleproject:596 TEST=angle_end2end_tests Change-Id: I846f2fc195ab1fd884052824ffd3c1d65083c0fb Reviewed-on: https://chromium-review.googlesource.com/322122Reviewed-by: 's avatarZhenyao Mo <zmo@chromium.org> Commit-Queue: Olli Etuaho <oetuaho@nvidia.com>
parent 9c757b12
......@@ -183,8 +183,8 @@ OutputHLSL::OutputHLSL(sh::GLenum shaderType, int shaderVersion,
mUniformHLSL->reserveUniformRegisters(3);
}
// Reserve registers for the default uniform block and driver constants
mUniformHLSL->reserveInterfaceBlockRegisters(2);
// Reserve registers for the default uniform block, driver constants and sampler metadata
mUniformHLSL->reserveInterfaceBlockRegisters(3);
}
OutputHLSL::~OutputHLSL()
......@@ -506,6 +506,11 @@ void OutputHLSL::header(TInfoSinkBase &out, const BuiltInFunctionEmulator *built
}
out << "};\n";
if (mOutputType == SH_HLSL_4_1_OUTPUT)
{
mUniformHLSL->samplerMetadataUniforms(out);
}
}
else
{
......@@ -607,6 +612,11 @@ void OutputHLSL::header(TInfoSinkBase &out, const BuiltInFunctionEmulator *built
out << "};\n"
"\n";
if (mOutputType == SH_HLSL_4_1_OUTPUT)
{
mUniformHLSL->samplerMetadataUniforms(out);
}
}
else
{
......
......@@ -246,6 +246,17 @@ void UniformHLSL::uniformsHeader(TInfoSinkBase &out,
}
}
void UniformHLSL::samplerMetadataUniforms(TInfoSinkBase &out)
{
if (mSamplerRegister > 0)
{
out << "cbuffer SamplerMetadata : register(b2)\n"
<< "{\n"
<< " int samplerMetadata[" << mSamplerRegister << "] : packoffset(c0);\n"
<< "};\n";
}
}
TString UniformHLSL::interfaceBlocksHeader(const ReferencedSymbols &referencedInterfaceBlocks)
{
TString interfaceBlocks;
......
......@@ -31,6 +31,10 @@ class UniformHLSL : angle::NonCopyable
void uniformsHeader(TInfoSinkBase &out,
ShShaderOutput outputType,
const ReferencedSymbols &referencedUniforms);
// Must be called after uniformsHeader
void samplerMetadataUniforms(TInfoSinkBase &out);
TString interfaceBlocksHeader(const ReferencedSymbols &referencedInterfaceBlocks);
// Used for direct index references
......
......@@ -22,6 +22,7 @@
#include "libANGLE/renderer/d3d/ShaderExecutableD3D.h"
#include "libANGLE/renderer/d3d/VaryingPacking.h"
#include "libANGLE/renderer/d3d/VertexDataManager.h"
#include "libANGLE/renderer/d3d/d3d11/renderer11_utils.h"
namespace rx
{
......@@ -487,6 +488,88 @@ const ShaderD3D *ProgramD3DMetadata::getFragmentShader() const
return mFragmentShader;
}
// SamplerMetadataD3D11 implementation
SamplerMetadataD3D11::SamplerMetadataD3D11()
: mSamplerCount(0), mType(gl::SAMPLER_VERTEX), mDirty(false), mSamplerMetadataBuffer(nullptr)
{
}
SamplerMetadataD3D11::~SamplerMetadataD3D11()
{
reset();
}
void SamplerMetadataD3D11::reset()
{
mDirty = false;
SafeRelease(mSamplerMetadataBuffer);
}
void SamplerMetadataD3D11::initData(unsigned int samplerCount, gl::SamplerType type)
{
mSamplerMetadata.resize(samplerCount);
mSamplerCount = samplerCount;
mType = type;
}
void SamplerMetadataD3D11::update(unsigned int samplerIndex, unsigned int baseLevel)
{
if (mSamplerMetadata[samplerIndex].baseLevel[0] != static_cast<int>(baseLevel))
{
mSamplerMetadata[samplerIndex].baseLevel[0] = static_cast<int>(baseLevel);
mDirty = true;
}
}
bool SamplerMetadataD3D11::initBuffer(ID3D11Device *device, ID3D11DeviceContext *deviceContext)
{
D3D11_BUFFER_DESC constantBufferDescription = {0};
d3d11::InitConstantBufferDesc(&constantBufferDescription,
sizeof(dx_SamplerMetadata) * mSamplerCount);
HRESULT result =
device->CreateBuffer(&constantBufferDescription, nullptr, &mSamplerMetadataBuffer);
ASSERT(SUCCEEDED(result));
if (FAILED(result))
{
return false;
}
if (mType == gl::SAMPLER_VERTEX)
{
deviceContext->VSSetConstantBuffers(d3d11::RESERVED_CONSTANT_BUFFER_SLOT_SAMPLER_METADATA,
1, &mSamplerMetadataBuffer);
}
else
{
deviceContext->PSSetConstantBuffers(d3d11::RESERVED_CONSTANT_BUFFER_SLOT_SAMPLER_METADATA,
1, &mSamplerMetadataBuffer);
}
mDirty = true;
return true;
}
gl::Error SamplerMetadataD3D11::apply(ID3D11Device *device, ID3D11DeviceContext *deviceContext)
{
if (!mSamplerMetadataBuffer && mSamplerCount > 0)
{
if (!initBuffer(device, deviceContext))
{
return gl::Error(GL_OUT_OF_MEMORY, "Failed to create shader constant buffer");
}
}
if (mDirty)
{
ASSERT(mSamplerMetadataBuffer);
if (mSamplerMetadataBuffer)
{
deviceContext->UpdateSubresource(mSamplerMetadataBuffer, 0, nullptr,
mSamplerMetadata.data(), 16, 0);
mDirty = false;
}
}
return gl::Error(GL_NO_ERROR);
}
// ProgramD3D Implementation
ProgramD3D::VertexExecutable::VertexExecutable(const gl::InputLayout &inputLayout,
......@@ -654,7 +737,29 @@ GLenum ProgramD3D::getSamplerTextureType(gl::SamplerType type, unsigned int samp
return GL_TEXTURE_2D;
}
GLint ProgramD3D::getUsedSamplerRange(gl::SamplerType type) const
void ProgramD3D::setSamplerMetadata(gl::SamplerType type,
unsigned int samplerIndex,
unsigned int baseLevel)
{
SamplerMetadataD3D11 *metadata = nullptr;
switch (type)
{
case gl::SAMPLER_PIXEL:
metadata = &mSamplerMetadataPS;
break;
case gl::SAMPLER_VERTEX:
metadata = &mSamplerMetadataVS;
break;
default:
UNREACHABLE();
break;
}
ASSERT(metadata != nullptr);
ASSERT(samplerIndex < getUsedSamplerRange(type));
metadata->update(samplerIndex, baseLevel);
}
GLuint ProgramD3D::getUsedSamplerRange(gl::SamplerType type) const
{
switch (type)
{
......@@ -664,7 +769,7 @@ GLint ProgramD3D::getUsedSamplerRange(gl::SamplerType type) const
return mUsedVertexSamplerRange;
default:
UNREACHABLE();
return 0;
return 0u;
}
}
......@@ -958,6 +1063,9 @@ LinkResult ProgramD3D::load(gl::InfoLog &infoLog, gl::BinaryInputStream *stream)
initializeUniformStorage();
initAttributesByLayout();
mSamplerMetadataPS.initData(getUsedSamplerRange(gl::SAMPLER_PIXEL), gl::SAMPLER_PIXEL);
mSamplerMetadataVS.initData(getUsedSamplerRange(gl::SAMPLER_VERTEX), gl::SAMPLER_VERTEX);
return LinkResult(true, gl::Error(GL_NO_ERROR));
}
......@@ -1440,6 +1548,9 @@ LinkResult ProgramD3D::link(const gl::Data &data, gl::InfoLog &infoLog)
defineUniformsAndAssignRegisters();
mSamplerMetadataPS.initData(getUsedSamplerRange(gl::SAMPLER_PIXEL), gl::SAMPLER_PIXEL);
mSamplerMetadataVS.initData(getUsedSamplerRange(gl::SAMPLER_VERTEX), gl::SAMPLER_VERTEX);
gatherTransformFeedbackVaryings(varyingPacking);
LinkResult result = compileProgramExecutables(data, infoLog);
......@@ -1564,6 +1675,11 @@ gl::Error ProgramD3D::applyUniforms(GLenum drawMode)
d3dUniform->dirty = false;
}
error = mRenderer->applySamplerMetadata(&mSamplerMetadataVS, mUsedVertexSamplerRange,
gl::SAMPLER_VERTEX);
error = mRenderer->applySamplerMetadata(&mSamplerMetadataPS, mUsedPixelSamplerRange,
gl::SAMPLER_PIXEL);
return gl::Error(GL_NO_ERROR);
}
......@@ -2140,6 +2256,9 @@ void ProgramD3D::reset()
mSamplersPS.clear();
mSamplersVS.clear();
mSamplerMetadataPS.reset();
mSamplerMetadataVS.reset();
mUsedVertexSamplerRange = 0;
mUsedPixelSamplerRange = 0;
mDirtySamplerMapping = true;
......
......@@ -128,6 +128,32 @@ class ProgramD3DMetadata : angle::NonCopyable
const ShaderD3D *mFragmentShader;
};
class SamplerMetadataD3D11 final : angle::NonCopyable
{
public:
SamplerMetadataD3D11();
~SamplerMetadataD3D11();
void reset();
void initData(unsigned int samplerCount, gl::SamplerType type);
void update(unsigned int samplerIndex, unsigned int baseLevel);
gl::Error apply(ID3D11Device *device, ID3D11DeviceContext *deviceContext);
private:
bool initBuffer(ID3D11Device *device, ID3D11DeviceContext *deviceContext);
struct dx_SamplerMetadata
{
int baseLevel[4];
};
std::vector<dx_SamplerMetadata> mSamplerMetadata;
unsigned int mSamplerCount;
gl::SamplerType mType;
bool mDirty;
ID3D11Buffer *mSamplerMetadataBuffer;
};
class ProgramD3D : public ProgramImpl
{
public:
......@@ -142,7 +168,7 @@ class ProgramD3D : public ProgramImpl
unsigned int samplerIndex,
const gl::Caps &caps) const;
GLenum getSamplerTextureType(gl::SamplerType type, unsigned int samplerIndex) const;
GLint getUsedSamplerRange(gl::SamplerType type) const;
GLuint getUsedSamplerRange(gl::SamplerType type) const;
void updateSamplerMapping();
bool usesPointSize() const { return mUsesPointSize; }
......@@ -174,6 +200,11 @@ class ProgramD3D : public ProgramImpl
bool getUniformBlockMemberInfo(const std::string &memberUniformName,
sh::BlockMemberInfo *memberInfoOut) const override;
// D3D11 needs sampler metadata to implement ESSL3 texture functions.
void setSamplerMetadata(gl::SamplerType type,
unsigned int samplerIndex,
unsigned int baseLevel);
void initializeUniformStorage();
gl::Error applyUniforms(GLenum drawMode);
gl::Error applyUniformBuffers(const gl::Data &data);
......@@ -401,6 +432,9 @@ class ProgramD3D : public ProgramImpl
static unsigned int issueSerial();
static unsigned int mCurrentSerial;
SamplerMetadataD3D11 mSamplerMetadataPS;
SamplerMetadataD3D11 mSamplerMetadataVS;
};
}
......
......@@ -284,7 +284,7 @@ gl::Error RendererD3D::generateSwizzles(const gl::Data &data, gl::SamplerType ty
{
ProgramD3D *programD3D = GetImplAs<ProgramD3D>(data.state->getProgram());
unsigned int samplerRange = static_cast<unsigned int>(programD3D->getUsedSamplerRange(type));
unsigned int samplerRange = programD3D->getUsedSamplerRange(type);
for (unsigned int i = 0; i < samplerRange; i++)
{
......@@ -419,6 +419,8 @@ gl::Error RendererD3D::applyTextures(const gl::Data &data, gl::SamplerType shade
{
return error;
}
programD3D->setSamplerMetadata(shaderType, samplerIndex, texture->getBaseLevel());
}
else
{
......
......@@ -44,6 +44,7 @@ class DeviceD3D;
class EGLImageD3D;
class ImageD3D;
class IndexBuffer;
class SamplerMetadataD3D11;
class ProgramD3D;
class RenderTargetD3D;
class ShaderExecutableD3D;
......@@ -161,6 +162,9 @@ class RendererD3D : public Renderer, public BufferFactoryD3D
virtual gl::Error applyUniforms(const ProgramD3D &programD3D,
GLenum drawMode,
const std::vector<D3DUniform *> &uniformArray) = 0;
virtual gl::Error applySamplerMetadata(SamplerMetadataD3D11 *samplerMetadata,
unsigned int samplerCount,
gl::SamplerType type) = 0;
virtual bool applyPrimitiveType(GLenum primitiveType, GLsizei elementCount, bool usesPointSize) = 0;
virtual gl::Error applyVertexBuffer(const gl::State &state, GLenum mode, GLint first, GLsizei count, GLsizei instances, SourceIndexData *sourceIndexInfo) = 0;
virtual gl::Error applyIndexBuffer(const gl::Data &data,
......@@ -341,7 +345,6 @@ struct dx_PixelConstants
float viewCoords[4];
float depthFront[4];
};
}
#endif // LIBANGLE_RENDERER_D3D_RENDERERD3D_H_
......@@ -2339,13 +2339,15 @@ gl::Error Renderer11::applyUniforms(const ProgramD3D &programD3D,
if (mCurrentVertexConstantBuffer != vertexConstantBuffer)
{
mDeviceContext->VSSetConstantBuffers(0, 1, &vertexConstantBuffer);
mDeviceContext->VSSetConstantBuffers(
d3d11::RESERVED_CONSTANT_BUFFER_SLOT_DEFAULT_UNIFORM_BLOCK, 1, &vertexConstantBuffer);
mCurrentVertexConstantBuffer = vertexConstantBuffer;
}
if (mCurrentPixelConstantBuffer != pixelConstantBuffer)
{
mDeviceContext->PSSetConstantBuffers(0, 1, &pixelConstantBuffer);
mDeviceContext->PSSetConstantBuffers(
d3d11::RESERVED_CONSTANT_BUFFER_SLOT_DEFAULT_UNIFORM_BLOCK, 1, &pixelConstantBuffer);
mCurrentPixelConstantBuffer = pixelConstantBuffer;
}
......@@ -2353,12 +2355,7 @@ gl::Error Renderer11::applyUniforms(const ProgramD3D &programD3D,
if (!mDriverConstantBufferVS)
{
D3D11_BUFFER_DESC constantBufferDescription = {0};
constantBufferDescription.ByteWidth = sizeof(dx_VertexConstants);
constantBufferDescription.Usage = D3D11_USAGE_DEFAULT;
constantBufferDescription.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDescription.CPUAccessFlags = 0;
constantBufferDescription.MiscFlags = 0;
constantBufferDescription.StructureByteStride = 0;
d3d11::InitConstantBufferDesc(&constantBufferDescription, sizeof(dx_VertexConstants));
HRESULT result = mDevice->CreateBuffer(&constantBufferDescription, NULL, &mDriverConstantBufferVS);
ASSERT(SUCCEEDED(result));
......@@ -2366,18 +2363,14 @@ gl::Error Renderer11::applyUniforms(const ProgramD3D &programD3D,
{
return gl::Error(GL_OUT_OF_MEMORY, "Failed to create vertex shader constant buffer, result: 0x%X.", result);
}
mDeviceContext->VSSetConstantBuffers(1, 1, &mDriverConstantBufferVS);
mDeviceContext->VSSetConstantBuffers(d3d11::RESERVED_CONSTANT_BUFFER_SLOT_DRIVER, 1,
&mDriverConstantBufferVS);
}
if (!mDriverConstantBufferPS)
{
D3D11_BUFFER_DESC constantBufferDescription = {0};
constantBufferDescription.ByteWidth = sizeof(dx_PixelConstants);
constantBufferDescription.Usage = D3D11_USAGE_DEFAULT;
constantBufferDescription.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDescription.CPUAccessFlags = 0;
constantBufferDescription.MiscFlags = 0;
constantBufferDescription.StructureByteStride = 0;
d3d11::InitConstantBufferDesc(&constantBufferDescription, sizeof(dx_PixelConstants));
HRESULT result = mDevice->CreateBuffer(&constantBufferDescription, NULL, &mDriverConstantBufferPS);
ASSERT(SUCCEEDED(result));
......@@ -2385,7 +2378,8 @@ gl::Error Renderer11::applyUniforms(const ProgramD3D &programD3D,
{
return gl::Error(GL_OUT_OF_MEMORY, "Failed to create pixel shader constant buffer, result: 0x%X.", result);
}
mDeviceContext->PSSetConstantBuffers(1, 1, &mDriverConstantBufferPS);
mDeviceContext->PSSetConstantBuffers(d3d11::RESERVED_CONSTANT_BUFFER_SLOT_DRIVER, 1,
&mDriverConstantBufferPS);
}
const dx_VertexConstants &vertexConstants = mStateManager.getVertexConstants();
......@@ -2430,6 +2424,18 @@ gl::Error Renderer11::applyUniforms(const ProgramD3D &programD3D,
return gl::Error(GL_NO_ERROR);
}
gl::Error Renderer11::applySamplerMetadata(SamplerMetadataD3D11 *samplerMetadata,
unsigned int samplerCount,
gl::SamplerType type)
{
if (mRenderer11DeviceCaps.featureLevel <= D3D_FEATURE_LEVEL_9_3)
{
return gl::Error(GL_NO_ERROR);
}
return samplerMetadata->apply(mDevice, mDeviceContext);
}
void Renderer11::markAllStateDirty()
{
TRACE_EVENT0("gpu.angle", "Renderer11::markAllStateDirty");
......
......@@ -135,6 +135,9 @@ class Renderer11 : public RendererD3D
gl::Error applyUniforms(const ProgramD3D &programD3D,
GLenum drawMode,
const std::vector<D3DUniform *> &uniformArray) override;
gl::Error applySamplerMetadata(SamplerMetadataD3D11 *samplerMetadata,
unsigned int samplerCount,
gl::SamplerType type) override;
virtual gl::Error applyVertexBuffer(const gl::State &state, GLenum mode, GLint first, GLsizei count, GLsizei instances, SourceIndexData *sourceIndexInfo);
gl::Error applyIndexBuffer(const gl::Data &data,
const GLvoid *indices,
......
......@@ -751,21 +751,19 @@ static size_t GetMaximumVertexUniformVectors(D3D_FEATURE_LEVEL featureLevel)
}
}
static size_t GetReservedVertexUniformBuffers()
{
// Reserve one buffer for the application uniforms, and one for driver uniforms
return 2;
}
static size_t GetMaximumVertexUniformBlocks(D3D_FEATURE_LEVEL featureLevel)
{
switch (featureLevel)
{
case D3D_FEATURE_LEVEL_11_1:
case D3D_FEATURE_LEVEL_11_0: return D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT - GetReservedVertexUniformBuffers();
case D3D_FEATURE_LEVEL_11_0:
return D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT -
d3d11::RESERVED_CONSTANT_BUFFER_SLOT_COUNT;
case D3D_FEATURE_LEVEL_10_1:
case D3D_FEATURE_LEVEL_10_0: return D3D10_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT - GetReservedVertexUniformBuffers();
case D3D_FEATURE_LEVEL_10_0:
return D3D10_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT -
d3d11::RESERVED_CONSTANT_BUFFER_SLOT_COUNT;
// Uniform blocks not supported on D3D11 Feature Level 9
case D3D_FEATURE_LEVEL_9_3:
......@@ -868,21 +866,19 @@ static size_t GetMaximumPixelUniformVectors(D3D_FEATURE_LEVEL featureLevel)
}
}
static size_t GetReservedPixelUniformBuffers()
{
// Reserve one buffer for the application uniforms, and one for driver uniforms
return 2;
}
static size_t GetMaximumPixelUniformBlocks(D3D_FEATURE_LEVEL featureLevel)
{
switch (featureLevel)
{
case D3D_FEATURE_LEVEL_11_1:
case D3D_FEATURE_LEVEL_11_0: return D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT - GetReservedPixelUniformBuffers();
case D3D_FEATURE_LEVEL_11_0:
return D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT -
d3d11::RESERVED_CONSTANT_BUFFER_SLOT_COUNT;
case D3D_FEATURE_LEVEL_10_1:
case D3D_FEATURE_LEVEL_10_0: return D3D10_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT - GetReservedPixelUniformBuffers();
case D3D_FEATURE_LEVEL_10_0:
return D3D10_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT -
d3d11::RESERVED_CONSTANT_BUFFER_SLOT_COUNT;
// Uniform blocks not supported on D3D11 Feature Level 9
case D3D_FEATURE_LEVEL_9_3:
......@@ -1489,6 +1485,16 @@ WorkaroundsD3D GenerateWorkarounds(D3D_FEATURE_LEVEL featureLevel)
return workarounds;
}
void InitConstantBufferDesc(D3D11_BUFFER_DESC *constantBufferDescription, size_t byteWidth)
{
constantBufferDescription->ByteWidth = static_cast<UINT>(byteWidth);
constantBufferDescription->Usage = D3D11_USAGE_DEFAULT;
constantBufferDescription->BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDescription->CPUAccessFlags = 0;
constantBufferDescription->MiscFlags = 0;
constantBufferDescription->StructureByteStride = 0;
}
} // namespace d3d11
TextureHelper11::TextureHelper11()
......
......@@ -334,6 +334,17 @@ void SetBufferData(ID3D11DeviceContext *context, ID3D11Buffer *constantBuffer, c
}
WorkaroundsD3D GenerateWorkarounds(D3D_FEATURE_LEVEL featureLevel);
enum ReservedConstantBufferSlot
{
RESERVED_CONSTANT_BUFFER_SLOT_DEFAULT_UNIFORM_BLOCK = 0,
RESERVED_CONSTANT_BUFFER_SLOT_DRIVER = 1,
RESERVED_CONSTANT_BUFFER_SLOT_SAMPLER_METADATA = 2,
RESERVED_CONSTANT_BUFFER_SLOT_COUNT = 3
};
void InitConstantBufferDesc(D3D11_BUFFER_DESC *constantBufferDescription, size_t byteWidth);
} // namespace d3d11
// A helper class which wraps a 2D or 3D texture.
......
......@@ -1700,6 +1700,13 @@ gl::Error Renderer9::applyUniforms(const ProgramD3D &programD3D,
return gl::Error(GL_NO_ERROR);
}
gl::Error Renderer9::applySamplerMetadata(SamplerMetadataD3D11 *samplerMetadata,
unsigned int samplerCount,
gl::SamplerType type)
{
return gl::Error(GL_NO_ERROR);
}
void Renderer9::applyUniformnfv(const D3DUniform *targetUniform, const GLfloat *v)
{
if (targetUniform->isReferencedByFragmentShader())
......
......@@ -120,6 +120,9 @@ class Renderer9 : public RendererD3D
gl::Error applyUniforms(const ProgramD3D &programD3D,
GLenum drawMode,
const std::vector<D3DUniform *> &uniformArray) override;
gl::Error applySamplerMetadata(SamplerMetadataD3D11 *samplerMetadata,
unsigned int samplerCount,
gl::SamplerType type) override;
virtual bool applyPrimitiveType(GLenum primitiveType, GLsizei elementCount, bool usesPointSize);
virtual gl::Error applyVertexBuffer(const gl::State &state, GLenum mode, GLint first, GLsizei count, GLsizei instances, SourceIndexData *sourceInfo);
gl::Error applyIndexBuffer(const gl::Data &data,
......
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