| // | |
| // Copyright 2017 The ANGLE Project Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| // | |
| // WebGLReadOutsideFramebufferTest.cpp : Test functions which read the framebuffer (readPixels, | |
| // copyTexSubImage2D, copyTexImage2D) on areas outside the framebuffer. | |
| #include "test_utils/ANGLETest.h" | |
| #include "test_utils/gl_raii.h" | |
| namespace | |
| { | |
| class PixelRect | |
| { | |
| public: | |
| PixelRect(int width, int height) : mWidth(width), mHeight(height), mData(width * height) {} | |
| // Set each pixel to a different color consisting of the x,y position and a given tag. | |
| // Making each pixel a different means any misplaced pixel will cause a failure. | |
| // Encoding the position proved valuable in debugging. | |
| void fill(unsigned tag) | |
| { | |
| for (int x = 0; x < mWidth; ++x) | |
| { | |
| for (int y = 0; y < mHeight; ++y) | |
| { | |
| mData[x + y * mWidth] = angle::GLColor(x + (y << 8) + (tag << 16)); | |
| } | |
| } | |
| } | |
| void toTexture2D(GLuint target, GLuint texid) const | |
| { | |
| glBindTexture(target, texid); | |
| if (target == GL_TEXTURE_CUBE_MAP) | |
| { | |
| glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| } | |
| else | |
| { | |
| ASSERT_GLENUM_EQ(GL_TEXTURE_2D, target); | |
| glTexImage2D(target, 0, GL_RGBA, mWidth, mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, | |
| mData.data()); | |
| } | |
| glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | |
| glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | |
| glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| } | |
| void toTexture3D(GLuint texid, GLint depth) const | |
| { | |
| glBindTexture(GL_TEXTURE_3D, texid); | |
| glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, mWidth, mHeight, depth, 0, GL_RGBA, | |
| GL_UNSIGNED_BYTE, nullptr); | |
| for (GLint z = 0; z < depth; z++) | |
| { | |
| glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, z, mWidth, mHeight, 1, GL_RGBA, | |
| GL_UNSIGNED_BYTE, mData.data()); | |
| } | |
| glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | |
| glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | |
| glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
| glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); | |
| } | |
| void readFB(int x, int y) | |
| { | |
| glReadPixels(x, y, mWidth, mHeight, GL_RGBA, GL_UNSIGNED_BYTE, mData.data()); | |
| } | |
| // Read pixels from 'other' into 'this' from position (x,y). | |
| // Pixels outside 'other' are untouched or zeroed according to 'zeroOutside.' | |
| void readPixelRect(const PixelRect &other, int x, int y, bool zeroOutside) | |
| { | |
| for (int i = 0; i < mWidth; ++i) | |
| { | |
| for (int j = 0; j < mHeight; ++j) | |
| { | |
| angle::GLColor *dest = &mData[i + j * mWidth]; | |
| if (!other.getPixel(x + i, y + j, dest) && zeroOutside) | |
| { | |
| *dest = angle::GLColor(0); | |
| } | |
| } | |
| } | |
| } | |
| bool getPixel(int x, int y, angle::GLColor *colorOut) const | |
| { | |
| if (0 <= x && x < mWidth && 0 <= y && y < mHeight) | |
| { | |
| *colorOut = mData[x + y * mWidth]; | |
| return true; | |
| } | |
| return false; | |
| } | |
| void compare(const PixelRect &expected) const | |
| { | |
| ASSERT_EQ(mWidth, expected.mWidth); | |
| ASSERT_EQ(mHeight, expected.mHeight); | |
| for (int x = 0; x < mWidth; ++x) | |
| { | |
| for (int y = 0; y < mHeight; ++y) | |
| { | |
| ASSERT_EQ(expected.mData[x + y * mWidth], mData[x + y * mWidth]) | |
| << "at (" << x << ", " << y << ")"; | |
| } | |
| } | |
| } | |
| private: | |
| int mWidth, mHeight; | |
| std::vector<angle::GLColor> mData; | |
| }; | |
| } // namespace | |
| namespace angle | |
| { | |
| class WebGLReadOutsideFramebufferTest : public ANGLETest | |
| { | |
| public: | |
| // Read framebuffer to 'pixelsOut' via glReadPixels. | |
| void TestReadPixels(int x, int y, int, PixelRect *pixelsOut) { pixelsOut->readFB(x, y); } | |
| // Read framebuffer to 'pixelsOut' via glCopyTexSubImage2D and GL_TEXTURE_2D. | |
| void TestCopyTexSubImage2D(int x, int y, int, PixelRect *pixelsOut) | |
| { | |
| // Init texture with given pixels. | |
| GLTexture destTexture; | |
| pixelsOut->toTexture2D(GL_TEXTURE_2D, destTexture.get()); | |
| // Read framebuffer -> texture -> 'pixelsOut' | |
| glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, x, y, kReadWidth, kReadHeight); | |
| readTexture2D(GL_TEXTURE_2D, destTexture.get(), kReadWidth, kReadHeight, pixelsOut); | |
| } | |
| // Read framebuffer to 'pixelsOut' via glCopyTexSubImage2D and cube map. | |
| void TestCopyTexSubImageCube(int x, int y, int, PixelRect *pixelsOut) | |
| { | |
| // Init texture with given pixels. | |
| GLTexture destTexture; | |
| pixelsOut->toTexture2D(GL_TEXTURE_CUBE_MAP, destTexture.get()); | |
| // Read framebuffer -> texture -> 'pixelsOut' | |
| glCopyTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, 0, 0, x, y, kReadWidth, kReadHeight); | |
| readTexture2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, destTexture.get(), kReadWidth, kReadHeight, | |
| pixelsOut); | |
| } | |
| // Read framebuffer to 'pixelsOut' via glCopyTexSubImage3D. | |
| void TestCopyTexSubImage3D(int x, int y, int z, PixelRect *pixelsOut) | |
| { | |
| // Init texture with given pixels. | |
| GLTexture destTexture; | |
| pixelsOut->toTexture3D(destTexture.get(), kTextureDepth); | |
| // Read framebuffer -> texture -> 'pixelsOut' | |
| glCopyTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, z, x, y, kReadWidth, kReadHeight); | |
| readTexture3D(destTexture, kReadWidth, kReadHeight, z, pixelsOut); | |
| } | |
| // Read framebuffer to 'pixelsOut' via glCopyTexImage2D and GL_TEXTURE_2D. | |
| void TestCopyTexImage2D(int x, int y, int, PixelRect *pixelsOut) | |
| { | |
| // Init texture with given pixels. | |
| GLTexture destTexture; | |
| pixelsOut->toTexture2D(GL_TEXTURE_2D, destTexture.get()); | |
| // Read framebuffer -> texture -> 'pixelsOut' | |
| glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, kReadWidth, kReadHeight, 0); | |
| readTexture2D(GL_TEXTURE_2D, destTexture.get(), kReadWidth, kReadHeight, pixelsOut); | |
| } | |
| // Read framebuffer to 'pixelsOut' via glCopyTexImage2D and cube map. | |
| void TestCopyTexImageCube(int x, int y, int, PixelRect *pixelsOut) | |
| { | |
| // Init texture with given pixels. | |
| GLTexture destTexture; | |
| pixelsOut->toTexture2D(GL_TEXTURE_CUBE_MAP, destTexture.get()); | |
| // Read framebuffer -> texture -> 'pixelsOut' | |
| glCopyTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, x, y, kReadWidth, kReadHeight, | |
| 0); | |
| readTexture2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, destTexture, kReadWidth, kReadHeight, | |
| pixelsOut); | |
| } | |
| protected: | |
| static constexpr int kFbWidth = 128; | |
| static constexpr int kFbHeight = 128; | |
| static constexpr int kTextureDepth = 16; | |
| static constexpr int kReadWidth = 4; | |
| static constexpr int kReadHeight = 4; | |
| static constexpr int kReadLayer = 2; | |
| // Tag the framebuffer pixels differently than the initial read buffer pixels, so we know for | |
| // sure which pixels are changed by reading. | |
| static constexpr GLuint fbTag = 0x1122; | |
| static constexpr GLuint readTag = 0xaabb; | |
| WebGLReadOutsideFramebufferTest() : mFBData(kFbWidth, kFbHeight) | |
| { | |
| setWindowWidth(kFbWidth); | |
| setWindowHeight(kFbHeight); | |
| setConfigRedBits(8); | |
| setConfigGreenBits(8); | |
| setConfigBlueBits(8); | |
| setConfigAlphaBits(8); | |
| setWebGLCompatibilityEnabled(true); | |
| } | |
| void testSetUp() override | |
| { | |
| constexpr char kVS[] = R"( | |
| attribute vec3 a_position; | |
| varying vec2 v_texCoord; | |
| void main() { | |
| v_texCoord = a_position.xy * 0.5 + 0.5; | |
| gl_Position = vec4(a_position, 1); | |
| })"; | |
| constexpr char kFS[] = R"( | |
| precision mediump float; | |
| varying vec2 v_texCoord; | |
| uniform sampler2D u_texture; | |
| void main() { | |
| gl_FragColor = texture2D(u_texture, v_texCoord); | |
| })"; | |
| mProgram = CompileProgram(kVS, kFS); | |
| glUseProgram(mProgram); | |
| GLint uniformLoc = glGetUniformLocation(mProgram, "u_texture"); | |
| ASSERT_NE(-1, uniformLoc); | |
| glUniform1i(uniformLoc, 0); | |
| glDisable(GL_BLEND); | |
| glDisable(GL_DEPTH_TEST); | |
| // fill framebuffer with unique pixels | |
| mFBData.fill(fbTag); | |
| GLTexture fbTexture; | |
| mFBData.toTexture2D(GL_TEXTURE_2D, fbTexture); | |
| drawQuad(mProgram, "a_position", 0.0f, 1.0f, true); | |
| } | |
| void testTearDown() override { glDeleteProgram(mProgram); } | |
| using TestFunc = void (WebGLReadOutsideFramebufferTest::*)(int x, | |
| int y, | |
| int z, | |
| PixelRect *dest); | |
| void Main2D(TestFunc testFunc, bool zeroOutside) { mainImpl(testFunc, zeroOutside, 0); } | |
| void Main3D(TestFunc testFunc, bool zeroOutside) | |
| { | |
| mainImpl(testFunc, zeroOutside, kReadLayer); | |
| } | |
| void mainImpl(TestFunc testFunc, bool zeroOutside, int readLayer) | |
| { | |
| PixelRect actual(kReadWidth, kReadHeight); | |
| PixelRect expected(kReadWidth, kReadHeight); | |
| // Read a kReadWidth*kReadHeight rectangle of pixels from places that include: | |
| // - completely outside framebuffer, on all sides of it (i,j < 0 or > 2) | |
| // - completely inside framebuffer (i,j == 1) | |
| // - straddling framebuffer boundary, at each corner and side | |
| for (int i = -1; i < 4; ++i) | |
| { | |
| for (int j = -1; j < 4; ++j) | |
| { | |
| int x = i * kFbWidth / 2 - kReadWidth / 2; | |
| int y = j * kFbHeight / 2 - kReadHeight / 2; | |
| // Put unique pixel values into the read destinations. | |
| actual.fill(readTag); | |
| expected.readPixelRect(actual, 0, 0, false); | |
| // Read from framebuffer into 'actual.' | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| (this->*testFunc)(x, y, readLayer, &actual); | |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); | |
| // Simulate framebuffer read, into 'expected.' | |
| expected.readPixelRect(mFBData, x, y, zeroOutside); | |
| // See if they are the same. | |
| actual.compare(expected); | |
| } | |
| } | |
| } | |
| // Get contents of given texture by drawing it into a framebuffer then reading with | |
| // glReadPixels(). | |
| void readTexture2D(GLuint target, GLuint texture, GLsizei width, GLsizei height, PixelRect *out) | |
| { | |
| GLFramebuffer fbo; | |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); | |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, texture, 0); | |
| out->readFB(0, 0); | |
| } | |
| // Get contents of current texture by drawing it into a framebuffer then reading with | |
| // glReadPixels(). | |
| void readTexture3D(GLuint texture, GLsizei width, GLsizei height, int zSlice, PixelRect *out) | |
| { | |
| GLFramebuffer fbo; | |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); | |
| glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0, zSlice); | |
| out->readFB(0, 0); | |
| } | |
| PixelRect mFBData; | |
| GLuint mProgram; | |
| }; | |
| class WebGL2ReadOutsideFramebufferTest : public WebGLReadOutsideFramebufferTest | |
| {}; | |
| // Check that readPixels does not set a destination pixel when | |
| // the corresponding source pixel is outside the framebuffer. | |
| TEST_P(WebGLReadOutsideFramebufferTest, ReadPixels) | |
| { | |
| Main2D(&WebGLReadOutsideFramebufferTest::TestReadPixels, false); | |
| } | |
| // Check that copyTexSubImage2D does not set a destination pixel when | |
| // the corresponding source pixel is outside the framebuffer. | |
| TEST_P(WebGLReadOutsideFramebufferTest, CopyTexSubImage2D) | |
| { | |
| Main2D(&WebGLReadOutsideFramebufferTest::TestCopyTexSubImage2D, false); | |
| // TODO(fjhenigman): Figure out this failure. | |
| // Cube map skipped on 64-bit Windows with D3D FL 9.3 | |
| ANGLE_SKIP_TEST_IF(GetParam() == ES2_D3D11_FL9_3()); | |
| Main2D(&WebGLReadOutsideFramebufferTest::TestCopyTexSubImageCube, false); | |
| } | |
| // Check that copyTexImage2D sets (0,0,0,0) for pixels outside the framebuffer. | |
| TEST_P(WebGLReadOutsideFramebufferTest, CopyTexImage2D) | |
| { | |
| // http://anglebug.com/4092 | |
| ANGLE_SKIP_TEST_IF(IsVulkan() || IsD3D9() || IsD3D11()); | |
| Main2D(&WebGLReadOutsideFramebufferTest::TestCopyTexImage2D, true); | |
| // TODO(fjhenigman): Figure out this failure. | |
| // Cube map skipped on 64-bit Windows with D3D FL 9.3 | |
| ANGLE_SKIP_TEST_IF(GetParam() == ES2_D3D11_FL9_3()); | |
| Main2D(&WebGLReadOutsideFramebufferTest::TestCopyTexImageCube, true); | |
| } | |
| // Check that copyTexSubImage3D does not set a destination pixel when | |
| // the corresponding source pixel is outside the framebuffer. | |
| TEST_P(WebGL2ReadOutsideFramebufferTest, CopyTexSubImage3D) | |
| { | |
| // TODO(hqle): Metal doesn't implement 3D texture yet. | |
| // http://anglebug.com/4136 (ES2 renderer is mistakenly included in this test) | |
| ANGLE_SKIP_TEST_IF(IsMetal()); | |
| // http://anglebug.com/4092 | |
| ANGLE_SKIP_TEST_IF(IsVulkan() || IsD3D9() || IsD3D11()); | |
| // Robust CopyTexSubImage3D behaviour is not implemented on OpenGL. | |
| ANGLE_SKIP_TEST_IF(IsDesktopOpenGL() || IsOpenGLES()); | |
| Main3D(&WebGLReadOutsideFramebufferTest::TestCopyTexSubImage3D, false); | |
| } | |
| ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(WebGLReadOutsideFramebufferTest); | |
| ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(WebGL2ReadOutsideFramebufferTest); | |
| } // namespace angle |