/*------------------------------------------------------------------------- * 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 #include #include #include #include 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 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)} , m_texture (0) { } 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 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(m_width - 1); const float divY = static_cast(m_height - 1); for (int x = 0; x < m_width; ++x) for (int y = 0; y < m_height; ++y) { const float colorX = static_cast(x) / divX; const float colorY = static_cast(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(m_width); const float offsetHeight = offset / static_cast(m_height); minU += m_offsetSign * offsetWidth; maxU += m_offsetSign * offsetWidth; minV += m_offsetSign * offsetHeight; maxV += m_offsetSign * offsetHeight; const std::vector positions = { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f }; const std::vector texCoords = { minU, minV, minU, maxV, maxU, minV, maxU, maxV }; const std::vector quadIndices = { 0, 1, 2, 2, 1, 3 }; const std::vector 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(vertexArrays.size()), vertexArrays.data(), glu::pr::TriangleStrip(static_cast(quadIndices.size()), quadIndices.data())); } bool NearestEdgeTestCase::verifyResults () { const auto& gl = m_context.getRenderContext().getFunctions(); std::vector 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 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 kDirections = { OffsetDirection::LEFT, OffsetDirection::RIGHT }; for (const auto direction : kDirections) addChild(new NearestEdgeTestCase{m_context, direction}); } } /* glcts namespace */