Commit c39a19aa by Martin Radev

Select viewport index in the GLSL/ESSL vertex shader

The patch enables viewport selection for multiview rendering in the GLSL/ESSL vertex shader through the use of the GL_NV_viewport_array2 extension. The AST is modified only for GLSL and ESSL to include the viewport selection expression after ViewID_OVR's initialization. BUG=angleproject:2062 TEST=angle_unittests Change-Id: Iee05bb5a4b687ed53ddbdd466f1572227b1f0cde
parent 56229f1b
......@@ -231,6 +231,15 @@ const ShCompileOptions SH_INITIALIZE_UNINITIALIZED_LOCALS = UINT64_C(1) << 32;
// ViewID_OVR is added as a varying variable to both the vertex and fragment shaders.
const ShCompileOptions SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW = UINT64_C(1) << 33;
// With the flag enabled the GLSL/ESSL vertex shader is modified to include code for viewport
// selection in the following way:
// - Code to enable the extension NV_viewport_array2 is included.
// - Code to select the viewport index is included at the beginning of main after ViewID_OVR's
// initialization: gl_ViewportIndex = int(ViewID_OVR)
// Note: The SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW flag also has to be enabled to have the
// temporary variable ViewID_OVR declared and initialized.
const ShCompileOptions SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER = UINT64_C(1) << 34;
// Defines alternate strategies for implementing array index clamping.
enum ShArrayIndexClampingStrategy
{
......
......@@ -218,6 +218,7 @@ int main(int argc, char *argv[])
case 'm':
resources.OVR_multiview = 1;
compileOptions |= SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW;
compileOptions |= SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER;
break;
case 'y': resources.EXT_YUV_target = 1; break;
default: failCode = EFailUsage;
......
......@@ -14,6 +14,7 @@
#include "angle_gl.h"
#include "compiler/translator/Compiler.h"
#include "compiler/translator/util.h"
using namespace sh;
......@@ -84,6 +85,14 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
return 0;
}
ShShaderOutput shaderOutput = static_cast<ShShaderOutput>(output);
if (!(IsOutputGLSL(shaderOutput) || IsOutputESSL(shaderOutput)) &&
(options & SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER) != 0u)
{
// This compiler option is only available in ESSL and GLSL.
return 0;
}
std::vector<uint32_t> validOutputs;
validOutputs.push_back(SH_ESSL_OUTPUT);
validOutputs.push_back(SH_GLSL_COMPATIBILITY_OUTPUT);
......@@ -125,8 +134,8 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
if (translators.find(key) == translators.end())
{
UniqueTCompiler translator(ConstructCompiler(type, static_cast<ShShaderSpec>(spec),
static_cast<ShShaderOutput>(output)));
UniqueTCompiler translator(
ConstructCompiler(type, static_cast<ShShaderSpec>(spec), shaderOutput));
if (translator == nullptr)
{
......
......@@ -537,7 +537,8 @@ enum TQualifier
EvqSecondaryFragColorEXT, // EXT_blend_func_extended
EvqSecondaryFragDataEXT, // EXT_blend_func_extended
EvqViewIDOVR, // OVR_multiview
EvqViewIDOVR, // OVR_multiview
EvqViewportIndex, // gl_ViewportIndex
// built-ins written by the shader_framebuffer_fetch extension(s)
EvqLastFragColor,
......@@ -774,6 +775,7 @@ inline const char *getQualifierString(TQualifier q)
case EvqSecondaryFragColorEXT: return "SecondaryFragColorEXT";
case EvqSecondaryFragDataEXT: return "SecondaryFragDataEXT";
case EvqViewIDOVR: return "ViewIDOVR";
case EvqViewportIndex: return "ViewportIndex";
case EvqLastFragColor: return "LastFragColor";
case EvqLastFragData: return "LastFragData";
case EvqSmoothOut: return "smooth out";
......
......@@ -431,7 +431,8 @@ TIntermBlock *TCompiler::compileTreeImpl(const char *const shaderStrings[],
if (success && (compileOptions & SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW) &&
parseContext.isMultiviewExtensionEnabled() && getShaderType() != GL_COMPUTE_SHADER)
{
DeclareAndInitBuiltinsForInstancedMultiview(root, getNumViews(), getShaderType());
DeclareAndInitBuiltinsForInstancedMultiview(root, mNumViews, shaderType, compileOptions,
outputType);
}
// This pass might emit short circuits so keep it before the short circuit unfolding
......
......@@ -13,6 +13,7 @@
#include "compiler/translator/InitializeVariables.h"
#include "compiler/translator/IntermTraverse.h"
#include "compiler/translator/SymbolTable.h"
#include "compiler/translator/util.h"
namespace sh
{
......@@ -47,10 +48,11 @@ TIntermSymbol *CreateGLInstanceIDSymbol()
return new TIntermSymbol(0, "gl_InstanceID", TType(EbtInt, EbpHigh, EvqInstanceID));
}
void InitializeViewIDAndInstanceID(TIntermBlock *root,
TIntermTyped *viewIDSymbol,
// Adds the InstanceID and ViewID_OVR initializers to the end of the initializers' sequence.
void InitializeViewIDAndInstanceID(TIntermTyped *viewIDSymbol,
TIntermTyped *instanceIDSymbol,
unsigned numberOfViews)
unsigned numberOfViews,
TIntermSequence *initializers)
{
// Create a signed numberOfViews node.
TConstantUnion *numberOfViewsConstant = new TConstantUnion();
......@@ -65,6 +67,7 @@ void InitializeViewIDAndInstanceID(TIntermBlock *root,
// Create a InstanceID = gl_InstanceID / numberOfViews node.
TIntermBinary *instanceIDInitializer =
new TIntermBinary(EOpAssign, instanceIDSymbol->deepCopy(), normalizedInstanceID);
initializers->push_back(instanceIDInitializer);
// Create a uint(gl_InstanceID) node.
TIntermSequence *glInstanceIDSymbolCastArguments = new TIntermSequence();
......@@ -85,11 +88,7 @@ void InitializeViewIDAndInstanceID(TIntermBlock *root,
// Create a ViewID_OVR = uint(gl_InstanceID) % numberOfViews node.
TIntermBinary *viewIDInitializer =
new TIntermBinary(EOpAssign, viewIDSymbol->deepCopy(), normalizedViewID);
// Add initializers at the beginning of main().
TIntermBlock *mainBody = FindMainBody(root);
mainBody->getSequence()->insert(mainBody->getSequence()->begin(), instanceIDInitializer);
mainBody->getSequence()->insert(mainBody->getSequence()->begin(), viewIDInitializer);
initializers->push_back(viewIDInitializer);
}
// Replaces every occurrence of a symbol with the name specified in symbolName with newSymbolNode.
......@@ -108,11 +107,32 @@ void DeclareGlobalVariable(TIntermBlock *root, TIntermTyped *typedNode)
globalSequence->insert(globalSequence->begin(), declaration);
}
// Adds the expression gl_ViewportIndex = int(ViewID_OVR) to the end of the initializers' sequence.
void SelectViewportIndexInVertexShader(TIntermTyped *viewIDSymbol, TIntermSequence *initializers)
{
// Create a gl_ViewportIndex node.
TIntermSymbol *viewportIndexSymbol =
new TIntermSymbol(0, "gl_ViewportIndex", TType(EbtInt, EbpHigh, EvqViewportIndex));
// Create an int(ViewID_OVR) node.
TIntermSequence *viewIDSymbolCastArguments = new TIntermSequence();
viewIDSymbolCastArguments->push_back(viewIDSymbol);
TIntermAggregate *viewIDAsInt = TIntermAggregate::CreateConstructor(
TType(EbtInt, EbpHigh, EvqTemporary), viewIDSymbolCastArguments);
// Create a gl_ViewportIndex = int(ViewID_OVR) node.
TIntermBinary *viewIDInitializer =
new TIntermBinary(EOpAssign, viewportIndexSymbol, viewIDAsInt);
initializers->push_back(viewIDInitializer);
}
} // namespace
void DeclareAndInitBuiltinsForInstancedMultiview(TIntermBlock *root,
unsigned numberOfViews,
GLenum shaderType)
GLenum shaderType,
ShCompileOptions compileOptions,
ShShaderOutput shaderOutput)
{
ASSERT(shaderType == GL_VERTEX_SHADER || shaderType == GL_FRAGMENT_SHADER);
......@@ -132,7 +152,28 @@ void DeclareAndInitBuiltinsForInstancedMultiview(TIntermBlock *root,
instanceIDSymbol->setInternal(true);
DeclareGlobalVariable(root, instanceIDSymbol);
ReplaceSymbol(root, "gl_InstanceID", instanceIDSymbol);
InitializeViewIDAndInstanceID(root, viewIDSymbol, instanceIDSymbol, numberOfViews);
TIntermSequence *initializers = new TIntermSequence();
InitializeViewIDAndInstanceID(viewIDSymbol, instanceIDSymbol, numberOfViews, initializers);
// The AST transformation which adds the expression to select the viewport index should
// be done only for the GLSL and ESSL output.
const bool selectViewport =
(compileOptions & SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER) != 0u;
// Assert that if the viewport is selected in the vertex shader, then the output is
// either GLSL or ESSL.
ASSERT(!selectViewport || IsOutputGLSL(shaderOutput) || IsOutputESSL(shaderOutput));
if (selectViewport)
{
// Setting a value to gl_ViewportIndex should happen after ViewID_OVR's initialization.
SelectViewportIndexInVertexShader(viewIDSymbol->deepCopy(), initializers);
}
// Insert initializers at the beginning of main().
TIntermBlock *initializersBlock = new TIntermBlock();
initializersBlock->getSequence()->swap(*initializers);
TIntermBlock *mainBody = FindMainBody(root);
mainBody->getSequence()->insert(mainBody->getSequence()->begin(), initializersBlock);
}
}
......
......@@ -7,16 +7,21 @@
// - Add declaration of View_ID_OVR.
// - Replace every occurrence of gl_ViewID_OVR with ViewID_OVR, mark ViewID_OVR as internal and
// declare it as a flat varying.
//
// If the shader type is a vertex shader, the following AST transformations are applied:
// - Replace every occurrence of gl_InstanceID with InstanceID, mark InstanceID as internal and set
// its qualifier to EvqTemporary.
// - Add initializers of ViewID_OVR and InstanceID to the beginning of the body of main. The pass
// should be executed before any variables get collected so that usage of gl_InstanceID is recorded.
// - If the output is ESSL or GLSL and the SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is
// enabled, the expression "gl_ViewportIndex = int(ViewID_OVR)" is added after ViewID and InstanceID
// are initialized.
//
#ifndef COMPILER_TRANSLATOR_DECLAREANDINITBUILTINSFORINSTANCEDMULTIVIEW_H_
#define COMPILER_TRANSLATOR_DECLAREANDINITBUILTINSFORINSTANCEDMULTIVIEW_H_
#include "GLSLANG/ShaderLang.h"
#include "angle_gl.h"
class TIntermBlock;
......@@ -26,7 +31,9 @@ namespace sh
void DeclareAndInitBuiltinsForInstancedMultiview(TIntermBlock *root,
unsigned numberOfViews,
GLenum shaderType);
GLenum shaderType,
ShCompileOptions compileOptions,
ShShaderOutput shaderOutput);
} // namespace sh
......
......@@ -121,14 +121,16 @@ void TranslatorESSL::writeExtensionBehavior(ShCompileOptions compileOptions)
TInfoSinkBase &sink = getInfoSink().obj;
const TExtensionBehavior &extBehavior = getExtensionBehavior();
const bool isMultiviewExtEmulated =
(compileOptions & (SH_TRANSLATE_VIEWID_OVR_TO_UNIFORM |
SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW)) != 0u;
(compileOptions &
(SH_TRANSLATE_VIEWID_OVR_TO_UNIFORM | SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW |
SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER)) != 0u;
for (TExtensionBehavior::const_iterator iter = extBehavior.begin(); iter != extBehavior.end();
++iter)
{
if (iter->second != EBhUndefined)
{
const bool isMultiview =
iter->first == "GL_OVR_multiview" || iter->first == "GL_OVR_multiview2";
if (getResources().NV_shader_framebuffer_fetch &&
iter->first == "GL_EXT_shader_framebuffer_fetch")
{
......@@ -140,11 +142,16 @@ void TranslatorESSL::writeExtensionBehavior(ShCompileOptions compileOptions)
sink << "#extension GL_NV_draw_buffers : " << getBehaviorString(iter->second)
<< "\n";
}
else if (isMultiviewExtEmulated &&
(iter->first == "GL_OVR_multiview" || iter->first == "GL_OVR_multiview2"))
else if (isMultiview && isMultiviewExtEmulated)
{
// No output
continue;
if (getShaderType() == GL_VERTEX_SHADER &&
(compileOptions & SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER) != 0u)
{
// Emit the NV_viewport_array2 extension in a vertex shader if the
// SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is set and the
// OVR_multiview(2) extension is requested.
sink << "#extension GL_NV_viewport_array2 : require\n";
}
}
else
{
......
......@@ -53,7 +53,7 @@ void TranslatorGLSL::translate(TIntermBlock *root, ShCompileOptions compileOptio
writeVersion(root);
// Write extension behaviour as needed
writeExtensionBehavior(root);
writeExtensionBehavior(root, compileOptions);
// Write pragmas after extensions because some drivers consider pragmas
// like non-preprocessor tokens.
......@@ -242,7 +242,7 @@ void TranslatorGLSL::writeVersion(TIntermNode *root)
}
}
void TranslatorGLSL::writeExtensionBehavior(TIntermNode *root)
void TranslatorGLSL::writeExtensionBehavior(TIntermNode *root, ShCompileOptions compileOptions)
{
TInfoSinkBase &sink = getInfoSink().obj;
const TExtensionBehavior &extBehavior = getExtensionBehavior();
......@@ -269,6 +269,17 @@ void TranslatorGLSL::writeExtensionBehavior(TIntermNode *root)
<< "\n";
}
}
const bool isMultiview =
iter.first == "GL_OVR_multiview" || iter.first == "GL_OVR_multiview2";
if (isMultiview && getShaderType() == GL_VERTEX_SHADER &&
(compileOptions & SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER) != 0u)
{
// Emit the NV_viewport_array2 extension in a vertex shader if the
// SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is set and the OVR_multiview(2)
// extension is requested.
sink << "#extension GL_NV_viewport_array2 : require\n";
}
}
// GLSL ES 3 explicit location qualifiers need to use an extension before GLSL 330
......
......@@ -27,7 +27,7 @@ class TranslatorGLSL : public TCompiler
private:
void writeVersion(TIntermNode *root);
void writeExtensionBehavior(TIntermNode *root);
void writeExtensionBehavior(TIntermNode *root, ShCompileOptions compileOptions);
void conditionallyOutputInvariantDeclaration(const char *builtinVaryingName);
};
......
......@@ -116,22 +116,28 @@ class WEBGLMultiviewFragmentShaderTest : public ShaderCompileTreeTest
}
};
class WEBGLMultiviewVertexShaderOutputCodeTest : public MatchOutputCodeTest
class WEBGLMultiviewOutputCodeTest : public MatchOutputCodeTest
{
public:
WEBGLMultiviewVertexShaderOutputCodeTest()
: MatchOutputCodeTest(GL_VERTEX_SHADER, 0, SH_ESSL_OUTPUT)
WEBGLMultiviewOutputCodeTest(sh::GLenum shaderType)
: MatchOutputCodeTest(shaderType, 0, SH_ESSL_OUTPUT)
{
addOutputType(SH_GLSL_COMPATIBILITY_OUTPUT);
getResources()->OVR_multiview = 1;
getResources()->MaxViewsOVR = 4;
}
void requestHLSLOutput()
{
#if defined(ANGLE_ENABLE_HLSL)
addOutputType(SH_HLSL_4_1_OUTPUT);
#endif
getResources()->OVR_multiview = 1;
getResources()->MaxViewsOVR = 4;
}
bool foundInAllGLSLCode(const char *str)
{
return foundInCode(SH_GLSL_COMPATIBILITY_OUTPUT, str) && foundInCode(SH_ESSL_OUTPUT, str);
return foundInGLSLCode(str) && foundInESSLCode(str);
}
bool foundInHLSLCode(const char *stringToFind) const
......@@ -144,6 +150,26 @@ class WEBGLMultiviewVertexShaderOutputCodeTest : public MatchOutputCodeTest
}
};
class WEBGLMultiviewVertexShaderOutputCodeTest : public WEBGLMultiviewOutputCodeTest
{
public:
WEBGLMultiviewVertexShaderOutputCodeTest() : WEBGLMultiviewOutputCodeTest(GL_VERTEX_SHADER) {}
};
class WEBGLMultiviewFragmentShaderOutputCodeTest : public WEBGLMultiviewOutputCodeTest
{
public:
WEBGLMultiviewFragmentShaderOutputCodeTest() : WEBGLMultiviewOutputCodeTest(GL_FRAGMENT_SHADER)
{
}
};
class WEBGLMultiviewComputeShaderOutputCodeTest : public WEBGLMultiviewOutputCodeTest
{
public:
WEBGLMultiviewComputeShaderOutputCodeTest() : WEBGLMultiviewOutputCodeTest(GL_COMPUTE_SHADER) {}
};
void VariableOccursNTimes(TIntermBlock *root,
const TString &varName,
const TQualifier varQualifier,
......@@ -723,6 +749,7 @@ TEST_F(WEBGLMultiviewVertexShaderOutputCodeTest, ViewIDAndInstanceIDHaveCorrectV
" gl_Position.yzw = vec3(0., 0., 1.);\n"
" myInstance = gl_InstanceID;\n"
"}\n";
requestHLSLOutput();
compile(shaderString, SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW);
EXPECT_TRUE(foundInAllGLSLCode("webgl_angle_ViewID_OVR = (uint(gl_InstanceID) % 3u)"));
......@@ -810,4 +837,98 @@ TEST_F(WEBGLMultiviewVertexShaderTest, ViewIDDeclaredAsFlatOutput)
VariableOccursNTimes(mASTRoot, "ViewID_OVR", EvqFlatOut, 2u);
}
} // namespace
// The test checks that the GL_NV_viewport_array2 extension is emitted in a vertex shader if the
// SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is set.
TEST_F(WEBGLMultiviewVertexShaderOutputCodeTest, ViewportArray2IsEmitted)
{
const std::string &shaderString =
"#version 300 es\n"
"#extension GL_OVR_multiview : require\n"
"layout(num_views = 3) in;\n"
"void main()\n"
"{\n"
"}\n";
compile(shaderString, SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW |
SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER);
EXPECT_TRUE(foundInAllGLSLCode("#extension GL_NV_viewport_array2 : require"));
}
// The test checks that the GL_NV_viewport_array2 extension is not emitted in a vertex shader if the
// OVR_multiview extension is not requested in the shader source even if the
// SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is set.
TEST_F(WEBGLMultiviewVertexShaderOutputCodeTest, ViewportArray2IsNotEmitted)
{
const std::string &shaderString =
"#version 300 es\n"
"void main()\n"
"{\n"
"}\n";
compile(shaderString, SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW |
SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER);
EXPECT_FALSE(foundInGLSLCode("#extension GL_NV_viewport_array2"));
EXPECT_FALSE(foundInESSLCode("#extension GL_NV_viewport_array2"));
}
// The test checks that the GL_NV_viewport_array2 extension is not emitted in a fragment shader if
// the SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is set.
TEST_F(WEBGLMultiviewFragmentShaderOutputCodeTest, ViewportArray2IsNotEmitted)
{
const std::string &shaderString =
"#version 300 es\n"
"#extension GL_OVR_multiview : require\n"
"void main()\n"
"{\n"
"}\n";
compile(shaderString, SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW |
SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER);
EXPECT_FALSE(foundInGLSLCode("#extension GL_NV_viewport_array2"));
EXPECT_FALSE(foundInESSLCode("#extension GL_NV_viewport_array2"));
}
// The test checks that the GL_NV_viewport_array2 extension is not emitted in a compute shader if
// the SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER option is set.
TEST_F(WEBGLMultiviewComputeShaderOutputCodeTest, ViewportArray2IsNotEmitted)
{
const std::string &shaderString =
"#version 300 es\n"
"#extension GL_OVR_multiview : require\n"
"void main()\n"
"{\n"
"}\n";
compile(shaderString, SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW |
SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER);
EXPECT_FALSE(foundInGLSLCode("#extension GL_NV_viewport_array2"));
EXPECT_FALSE(foundInESSLCode("#extension GL_NV_viewport_array2"));
}
// The test checks that the viewport index is selected after the initialization of ViewID_OVR for
// GLSL and ESSL ouputs.
TEST_F(WEBGLMultiviewVertexShaderOutputCodeTest, GlViewportIndexIsSet)
{
const std::string &shaderString =
"#version 300 es\n"
"#extension GL_OVR_multiview : require\n"
"layout(num_views = 3) in;\n"
"void main()\n"
"{\n"
"}\n";
compile(shaderString, SH_INITIALIZE_BUILTINS_FOR_INSTANCED_MULTIVIEW |
SH_SELECT_VIEW_IN_NV_GLSL_VERTEX_SHADER);
const char glViewportIndexAssignment[] = "gl_ViewportIndex = int(webgl_angle_ViewID_OVR)";
// Check that the viewport index is selected.
EXPECT_TRUE(foundInAllGLSLCode(glViewportIndexAssignment));
// Setting gl_ViewportIndex must happen after ViewID_OVR's initialization.
const char viewIDOVRAssignment[] = "webgl_angle_ViewID_OVR = (uint(gl_InstanceID) % 3u)";
size_t viewIDOVRAssignmentLoc = findInCode(SH_GLSL_COMPATIBILITY_OUTPUT, viewIDOVRAssignment);
size_t glViewportIndexAssignmentLoc =
findInCode(SH_GLSL_COMPATIBILITY_OUTPUT, glViewportIndexAssignment);
EXPECT_LT(viewIDOVRAssignmentLoc, glViewportIndexAssignmentLoc);
viewIDOVRAssignmentLoc = findInCode(SH_ESSL_OUTPUT, viewIDOVRAssignment);
glViewportIndexAssignmentLoc = findInCode(SH_ESSL_OUTPUT, glViewportIndexAssignment);
EXPECT_LT(viewIDOVRAssignmentLoc, glViewportIndexAssignmentLoc);
}
} // namespace
\ No newline at end of file
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