| // |
| // 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(target == GL_TEXTURE_2D); |
| 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 SetUp() override |
| { |
| ANGLETest::SetUp(); |
| |
| // TODO(fjhenigman): Factor out this shader and others like it in other tests, into |
| // ANGLETest. |
| const std::string vertexShader = |
| "attribute vec3 a_position;\n" |
| "varying vec2 v_texCoord;\n" |
| "void main() {\n" |
| " v_texCoord = a_position.xy * 0.5 + 0.5;\n" |
| " gl_Position = vec4(a_position, 1);\n" |
| "}\n"; |
| const std::string fragmentShader = |
| "precision mediump float;\n" |
| "varying vec2 v_texCoord;\n" |
| "uniform sampler2D u_texture;\n" |
| "void main() {\n" |
| " gl_FragColor = texture2D(u_texture, v_texCoord);\n" |
| "}\n"; |
| |
| mProgram = CompileProgram(vertexShader, fragmentShader); |
| 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 TearDown() override |
| { |
| glDeleteProgram(mProgram); |
| ANGLETest::TearDown(); |
| } |
| |
| 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); |
| |
| #ifdef _WIN64 |
| // TODO(fjhenigman): Figure out this failure. |
| if (GetParam() == ES2_D3D11_FL9_3()) |
| { |
| std::cout << "Cube map skipped on 64-bit " << GetParam() << "." << std::endl; |
| return; |
| } |
| #endif |
| |
| Main2D(&WebGLReadOutsideFramebufferTest::TestCopyTexSubImageCube, false); |
| } |
| |
| // Check that copyTexImage2D sets (0,0,0,0) for pixels outside the framebuffer. |
| TEST_P(WebGLReadOutsideFramebufferTest, CopyTexImage2D) |
| { |
| Main2D(&WebGLReadOutsideFramebufferTest::TestCopyTexImage2D, true); |
| |
| #ifdef _WIN64 |
| // TODO(fjhenigman): Figure out this failure. |
| if (GetParam() == ES2_D3D11_FL9_3()) |
| { |
| std::cout << "Cube map skipped on 64-bit " << GetParam() << "." << std::endl; |
| return; |
| } |
| #endif |
| |
| 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) |
| { |
| if (IsDesktopOpenGL() || IsOpenGLES()) |
| { |
| std::cout << "Robust CopyTexSubImage3D behaviour is not implemented on OpenGL." |
| << std::endl; |
| return; |
| } |
| |
| Main3D(&WebGLReadOutsideFramebufferTest::TestCopyTexSubImage3D, false); |
| } |
| |
| ANGLE_INSTANTIATE_TEST(WebGLReadOutsideFramebufferTest, |
| ES2_D3D9(), |
| ES2_D3D11(), |
| ES3_D3D11(), |
| ES2_D3D11_FL9_3(), |
| ES2_OPENGL(), |
| ES3_OPENGL(), |
| ES2_OPENGLES(), |
| ES3_OPENGLES()); |
| |
| ANGLE_INSTANTIATE_TEST(WebGL2ReadOutsideFramebufferTest, |
| ES3_D3D11(), |
| ES3_OPENGL(), |
| ES2_OPENGLES(), |
| ES3_OPENGLES()); |
| |
| } // namespace |