blob: 5eb7bba78bda6d1ca6e8be7c4c18e2ee9c337df4 [file] [log] [blame]
/*-------------------------------------------------------------------------
* OpenGL Conformance Test Suite
* -----------------------------
*
* Copyright (c) 2020 Valve Coporation.
* Copyright (c) 2020 The Khronos Group Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/ /*!
* \file glcNearestEdgeTests.cpp
* \brief
*/ /*-------------------------------------------------------------------*/
#include "glcNearestEdgeTests.hpp"
#include "gluDefs.hpp"
#include "gluTextureUtil.hpp"
#include "gluDrawUtil.hpp"
#include "gluShaderProgram.hpp"
#include "glwDefs.hpp"
#include "glwFunctions.hpp"
#include "glwEnums.hpp"
#include "tcuTestLog.hpp"
#include "tcuRenderTarget.hpp"
#include "tcuStringTemplate.hpp"
#include "tcuTextureUtil.hpp"
#include <utility>
#include <map>
#include <algorithm>
#include <memory>
#include <cmath>
namespace glcts
{
namespace
{
enum class OffsetDirection
{
LEFT = 0,
RIGHT = 1,
};
// Test sampling at the edge of texels. This test is equivalent to:
// 1) Creating a texture using the same format and size as the frame buffer.
// 2) Drawing a full screen quad with GL_NEAREST using the texture.
// 3) Verifying the frame buffer image and the texture match pixel-by-pixel.
//
// However, texture coodinates are not located in the exact frame buffer corners. A small offset is applied instead so sampling
// happens near a texel border instead of in the middle of the texel.
class NearestEdgeTestCase : public deqp::TestCase
{
public:
NearestEdgeTestCase(deqp::Context& context, OffsetDirection direction);
void deinit();
void init();
tcu::TestNode::IterateResult iterate();
static std::string getName (OffsetDirection direction);
static std::string getDesc (OffsetDirection direction);
static tcu::TextureFormat toTextureFormat (deqp::Context& context, const tcu::PixelFormat& pixelFmt);
private:
static const glw::GLenum kTextureType = GL_TEXTURE_2D;
void createTexture ();
void deleteTexture ();
void fillTexture ();
void renderQuad ();
bool verifyResults ();
const float m_offsetSign;
const int m_width;
const int m_height;
const tcu::PixelFormat& m_format;
const tcu::TextureFormat m_texFormat;
const tcu::TextureFormatInfo m_texFormatInfo;
const glu::TransferFormat m_transFormat;
std::string m_vertShaderText;
std::string m_fragShaderText;
glw::GLuint m_texture;
std::vector<deUint8> m_texData;
};
std::string NearestEdgeTestCase::getName (OffsetDirection direction)
{
switch (direction)
{
case OffsetDirection::LEFT: return "offset_left";
case OffsetDirection::RIGHT: return "offset_right";
default: DE_ASSERT(false); break;
}
// Unreachable.
return "";
}
std::string NearestEdgeTestCase::getDesc (OffsetDirection direction)
{
switch (direction)
{
case OffsetDirection::LEFT: return "Sampling point near the left edge";
case OffsetDirection::RIGHT: return "Sampling point near the right edge";
default: DE_ASSERT(false); break;
}
// Unreachable.
return "";
}
// Translate pixel format in the frame buffer to texture format.
// Copied from sglrReferenceContext.cpp.
tcu::TextureFormat NearestEdgeTestCase::toTextureFormat (deqp::Context& context, const tcu::PixelFormat& pixelFmt)
{
static const struct
{
tcu::PixelFormat pixelFmt;
tcu::TextureFormat texFmt;
} pixelFormatMap[] =
{
{ tcu::PixelFormat(8,8,8,8), tcu::TextureFormat(tcu::TextureFormat::RGBA, tcu::TextureFormat::UNORM_INT8) },
{ tcu::PixelFormat(8,8,8,0), tcu::TextureFormat(tcu::TextureFormat::RGB, tcu::TextureFormat::UNORM_INT8) },
{ tcu::PixelFormat(4,4,4,4), tcu::TextureFormat(tcu::TextureFormat::RGBA, tcu::TextureFormat::UNORM_SHORT_4444) },
{ tcu::PixelFormat(5,5,5,1), tcu::TextureFormat(tcu::TextureFormat::RGBA, tcu::TextureFormat::UNORM_SHORT_5551) },
{ tcu::PixelFormat(5,6,5,0), tcu::TextureFormat(tcu::TextureFormat::RGB, tcu::TextureFormat::UNORM_SHORT_565) },
{ tcu::PixelFormat(10,10,10,2), tcu::TextureFormat(tcu::TextureFormat::RGBA, tcu::TextureFormat::UNORM_INT_1010102_REV) },
{ tcu::PixelFormat(16,16,16,16), tcu::TextureFormat(tcu::TextureFormat::RGBA, tcu::TextureFormat::HALF_FLOAT) },
};
for (int ndx = 0; ndx < DE_LENGTH_OF_ARRAY(pixelFormatMap); ndx++)
{
if (pixelFormatMap[ndx].pixelFmt == pixelFmt)
{
// Some implementations treat GL_RGB8 as GL_RGBA8888,so the test should pass implementation format to ReadPixels.
if (pixelFmt == tcu::PixelFormat(8, 8, 8, 0))
{
const auto& gl = context.getRenderContext().getFunctions();
glw::GLint implFormat = GL_NONE;
glw::GLint implType = GL_NONE;
gl.getIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
gl.getIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
if (implFormat == GL_RGBA && implType == GL_UNSIGNED_BYTE)
return tcu::TextureFormat(tcu::TextureFormat::RGBA, tcu::TextureFormat::UNORM_INT8);
}
return pixelFormatMap[ndx].texFmt;
}
}
TCU_FAIL("Unable to map pixel format to texture format");
}
NearestEdgeTestCase::NearestEdgeTestCase (deqp::Context& context, OffsetDirection direction)
: TestCase(context, getName(direction).c_str(), getDesc(direction).c_str())
, m_offsetSign {(direction == OffsetDirection::LEFT) ? -1.0f : 1.0f}
, m_width {context.getRenderTarget().getWidth()}
, m_height {context.getRenderTarget().getHeight()}
, m_format {context.getRenderTarget().getPixelFormat()}
, m_texFormat {toTextureFormat(context, m_format)}
, m_texFormatInfo {tcu::getTextureFormatInfo(m_texFormat)}
, m_transFormat {glu::getTransferFormat(m_texFormat)}
{
}
void NearestEdgeTestCase::deinit()
{
}
void NearestEdgeTestCase::init()
{
if (m_width < 2 || m_height < 2)
TCU_THROW(NotSupportedError, "Render target size too small");
m_vertShaderText =
"#version ${VERSION}\n"
"\n"
"in highp vec2 position;\n"
"\n"
"void main()\n"
"{\n"
" gl_Position = vec4(position, 0.0, 1.0);\n"
"}\n"
;
m_fragShaderText =
"#version ${VERSION}\n"
"\n"
"precision highp float;\n"
"out highp vec4 fragColor;\n"
"\n"
"uniform highp sampler2D texSampler;\n"
"uniform float texOffset;\n"
"uniform float texWidth;\n"
"uniform float texHeight;\n"
"\n"
"void main()\n"
"{\n"
" float texCoordX;\n"
" float texCoordY;\n"
" texCoordX = (gl_FragCoord.x + texOffset) / texWidth;\n "
" texCoordY = (gl_FragCoord.y + texOffset) / texHeight;\n"
" vec2 sampleCoord = vec2(texCoordX, texCoordY);\n"
" fragColor = texture(texSampler, sampleCoord);\n"
"}\n"
"\n";
tcu::StringTemplate vertShaderTemplate{m_vertShaderText};
tcu::StringTemplate fragShaderTemplate{m_fragShaderText};
std::map<std::string, std::string> replacements;
if (glu::isContextTypeGLCore(m_context.getRenderContext().getType()))
replacements["VERSION"] = "130";
else
replacements["VERSION"] = "300 es";
m_vertShaderText = vertShaderTemplate.specialize(replacements);
m_fragShaderText = fragShaderTemplate.specialize(replacements);
}
void NearestEdgeTestCase::createTexture ()
{
const auto& gl = m_context.getRenderContext().getFunctions();
gl.genTextures(1, &m_texture);
GLU_EXPECT_NO_ERROR(gl.getError(), "glGenTextures");
gl.bindTexture(kTextureType, m_texture);
GLU_EXPECT_NO_ERROR(gl.getError(), "glBindTexture");
gl.texParameteri(kTextureType, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
GLU_EXPECT_NO_ERROR(gl.getError(), "glTexParameteri");
gl.texParameteri(kTextureType, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
GLU_EXPECT_NO_ERROR(gl.getError(), "glTexParameteri");
gl.texParameteri(kTextureType, GL_TEXTURE_WRAP_S, GL_REPEAT);
GLU_EXPECT_NO_ERROR(gl.getError(), "glTexParameteri");
gl.texParameteri(kTextureType, GL_TEXTURE_WRAP_T, GL_REPEAT);
GLU_EXPECT_NO_ERROR(gl.getError(), "glTexParameteri");
gl.texParameteri(kTextureType, GL_TEXTURE_MAX_LEVEL, 0);
GLU_EXPECT_NO_ERROR(gl.getError(), "glTexParameteri");
}
void NearestEdgeTestCase::deleteTexture ()
{
const auto& gl = m_context.getRenderContext().getFunctions();
gl.deleteTextures(1, &m_texture);
GLU_EXPECT_NO_ERROR(gl.getError(), "glDeleteTextures");
}
void NearestEdgeTestCase::fillTexture ()
{
const auto& gl = m_context.getRenderContext().getFunctions();
m_texData.resize(m_width * m_height * tcu::getPixelSize(m_texFormat));
tcu::PixelBufferAccess texAccess{m_texFormat, m_width, m_height, 1, m_texData.data()};
// Create gradient over the whole texture.
DE_ASSERT(m_width > 1);
DE_ASSERT(m_height > 1);
const float divX = static_cast<float>(m_width - 1);
const float divY = static_cast<float>(m_height - 1);
for (int x = 0; x < m_width; ++x)
for (int y = 0; y < m_height; ++y)
{
const float colorX = static_cast<float>(x) / divX;
const float colorY = static_cast<float>(y) / divY;
const float colorZ = std::min(colorX, colorY);
tcu::Vec4 color{colorX, colorY, colorZ, 1.0f};
tcu::Vec4 finalColor = (color - m_texFormatInfo.lookupBias) / m_texFormatInfo.lookupScale;
texAccess.setPixel(finalColor, x, y);
}
const auto internalFormat = glu::getInternalFormat(m_texFormat);
if (tcu::getPixelSize(m_texFormat) < 4)
gl.pixelStorei(GL_UNPACK_ALIGNMENT, 1);
gl.texImage2D(kTextureType, 0, internalFormat, m_width, m_height, 0 /* border */, m_transFormat.format, m_transFormat.dataType, m_texData.data());
GLU_EXPECT_NO_ERROR(gl.getError(), "glTexImage2D");
}
// Draw full screen quad with the texture and an offset of almost half a texel in one direction, so sampling happens near the texel
// border and verifies truncation is happening properly.
void NearestEdgeTestCase::renderQuad ()
{
const auto& renderContext = m_context.getRenderContext();
const auto& gl = renderContext.getFunctions();
float minU = 0.0f;
float maxU = 1.0f;
float minV = 0.0f;
float maxV = 1.0f;
// Apply offset of almost half a texel to the texture coordinates.
DE_ASSERT(m_offsetSign == 1.0f || m_offsetSign == -1.0f);
const float offset = 0.5f - pow(2.0f, -8.0f);
const float offsetWidth = offset / static_cast<float>(m_width);
const float offsetHeight = offset / static_cast<float>(m_height);
minU += m_offsetSign * offsetWidth;
maxU += m_offsetSign * offsetWidth;
minV += m_offsetSign * offsetHeight;
maxV += m_offsetSign * offsetHeight;
const std::vector<float> positions = { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f };
const std::vector<float> texCoords = { minU, minV, minU, maxV, maxU, minV, maxU, maxV };
const std::vector<deUint16> quadIndices = { 0, 1, 2, 2, 1, 3 };
const std::vector<glu::VertexArrayBinding> vertexArrays =
{
glu::va::Float("position", 2, 4, 0, positions.data())
};
glu::ShaderProgram program(m_context.getRenderContext(), glu::makeVtxFragSources(m_vertShaderText, m_fragShaderText));
if (!program.isOk())
TCU_FAIL("Shader compilation failed");
gl.useProgram(program.getProgram());
GLU_EXPECT_NO_ERROR(gl.getError(), "glUseProgram failed");
gl.uniform1i(gl.getUniformLocation(program.getProgram(), "texSampler"), 0);
GLU_EXPECT_NO_ERROR(gl.getError(), "glUniform1i failed");
gl.uniform1f(gl.getUniformLocation(program.getProgram(), "texOffset"), m_offsetSign * offset);
gl.uniform1f(gl.getUniformLocation(program.getProgram(), "texWidth"), float(m_width));
gl.uniform1f(gl.getUniformLocation(program.getProgram(), "texHeight"), float(m_height));
GLU_EXPECT_NO_ERROR(gl.getError(), "glUniform1i failed");
gl.disable(GL_DITHER);
gl.clear(GL_COLOR_BUFFER_BIT);
glu::draw(renderContext, program.getProgram(),
static_cast<int>(vertexArrays.size()), vertexArrays.data(),
glu::pr::TriangleStrip(static_cast<int>(quadIndices.size()), quadIndices.data()));
}
bool NearestEdgeTestCase::verifyResults ()
{
const auto& gl = m_context.getRenderContext().getFunctions();
std::vector<deUint8> fbData(m_width * m_height * tcu::getPixelSize(m_texFormat));
if (tcu::getPixelSize(m_texFormat) < 4)
gl.pixelStorei(GL_PACK_ALIGNMENT, 1);
gl.readPixels(0, 0, m_width, m_height, m_transFormat.format, m_transFormat.dataType, fbData.data());
GLU_EXPECT_NO_ERROR(gl.getError(), "glReadPixels");
tcu::ConstPixelBufferAccess texAccess {m_texFormat, m_width, m_height, 1, m_texData.data()};
tcu::ConstPixelBufferAccess fbAccess {m_texFormat, m_width, m_height, 1, fbData.data()};
// Difference image to ease spotting problems.
const tcu::TextureFormat diffFormat {tcu::TextureFormat::RGBA, tcu::TextureFormat::UNORM_INT8};
const auto diffBytes = tcu::getPixelSize(diffFormat) * m_width * m_height;
std::unique_ptr<deUint8[]> diffData {new deUint8[diffBytes]};
const tcu::PixelBufferAccess diffAccess {diffFormat, m_width, m_height, 1, diffData.get()};
const tcu::Vec4 colorRed {1.0f, 0.0f, 0.0f, 1.0f};
const tcu::Vec4 colorGreen {0.0f, 1.0f, 0.0f, 1.0f};
bool pass = true;
for (int x = 0; x < m_width; ++x)
for (int y = 0; y < m_height; ++y)
{
const auto texPixel = texAccess.getPixel(x, y);
const auto fbPixel = fbAccess.getPixel(x, y);
// Require perfect pixel match.
if (texPixel != fbPixel)
{
pass = false;
diffAccess.setPixel(colorRed, x, y);
}
else
{
diffAccess.setPixel(colorGreen, x, y);
}
}
if (!pass)
{
auto& log = m_testCtx.getLog();
log
<< tcu::TestLog::Message << "\n"
<< "Width: " << m_width << "\n"
<< "Height: " << m_height << "\n"
<< tcu::TestLog::EndMessage;
log << tcu::TestLog::Image("texture", "Generated Texture", texAccess);
log << tcu::TestLog::Image("fb", "Frame Buffer Contents", fbAccess);
log << tcu::TestLog::Image("diff", "Mismatched pixels in red", diffAccess);
}
return pass;
}
tcu::TestNode::IterateResult NearestEdgeTestCase::iterate ()
{
// Populate and configure m_texture.
createTexture();
// Fill m_texture with data.
fillTexture();
// Draw full screen quad using the texture and a slight offset left or right.
renderQuad();
// Verify results.
bool pass = verifyResults();
// Destroy texture.
deleteTexture();
const qpTestResult result = (pass ? QP_TEST_RESULT_PASS : QP_TEST_RESULT_FAIL);
const char* desc = (pass ? "Pass" : "Pixel mismatch; check the generated images");
m_testCtx.setTestResult(result, desc);
return STOP;
}
} /* anonymous namespace */
NearestEdgeCases::NearestEdgeCases(deqp::Context& context)
: TestCaseGroup(context, "nearest_edge", "GL_NEAREST edge cases")
{
}
NearestEdgeCases::~NearestEdgeCases(void)
{
}
void NearestEdgeCases::init(void)
{
static const std::vector<OffsetDirection> kDirections = { OffsetDirection::LEFT, OffsetDirection::RIGHT };
for (const auto direction : kDirections)
addChild(new NearestEdgeTestCase{m_context, direction});
}
} /* glcts namespace */