| // |
| // Copyright 2022 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. |
| // |
| // Copyright 2022 Rive |
| // |
| |
| #include <regex> |
| #include <sstream> |
| #include <string> |
| #include "test_utils/ANGLETest.h" |
| #include "test_utils/gl_raii.h" |
| |
| using namespace angle; |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////// |
| // GL_ANGLE_shader_pixel_local_storage prototype. |
| // |
| // NOTE: the hope is for this to eventually move into ANGLE. |
| |
| #define GL_DISABLED_ANGLE 0xbaadbeef |
| |
| constexpr static int MAX_LOCAL_STORAGE_PLANES = 3; |
| constexpr static int MAX_LOCAL_STORAGE_BYTES = 16; |
| constexpr static int MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE = 1; |
| |
| // ES 3.1 unfortunately requires most image formats to be either readonly or writeonly. To work |
| // around this limitation, we bind the same image unit to both a readonly and a writeonly image2D. |
| // We mark the images as volatile since they are aliases of the same memory. |
| // |
| // The ANGLE GLSL compiler doesn't appear to support macro concatenation (e.g., NAME ## _R). For |
| // now, the client code is responsible to know there are two image2D variables, append "_R" for |
| // pixelLocalLoadImpl, and append "_W" for pixelLocalStoreImpl. |
| // |
| // NOTE: PixelLocalStorageTest::useProgram appends "_R"/"_W" for you automatically if you use |
| // PIXEL_LOCAL_DECL / pixelLocalLoad / pixelLocalStore. |
| constexpr static const char kLocalStorageGLSLDefines[] = R"( |
| #define PIXEL_LOCAL_DECL_IMPL(NAME_R, NAME_W, BINDING, FORMAT) \ |
| layout(BINDING, FORMAT) coherent volatile readonly highp uniform image2D NAME_R; \ |
| layout(BINDING, FORMAT) coherent volatile writeonly highp uniform image2D NAME_W |
| #define PIXEL_LOCAL_DECL_I_IMPL(NAME_R, NAME_W, BINDING, FORMAT) \ |
| layout(BINDING, FORMAT) coherent volatile readonly highp uniform iimage2D NAME_R; \ |
| layout(BINDING, FORMAT) coherent volatile writeonly highp uniform iimage2D NAME_W |
| #define PIXEL_LOCAL_DECL_UI_IMPL(NAME_R, NAME_W, BINDING, FORMAT) \ |
| layout(BINDING, FORMAT) coherent volatile readonly highp uniform uimage2D NAME_R; \ |
| layout(BINDING, FORMAT) coherent volatile writeonly highp uniform uimage2D NAME_W |
| #define PIXEL_I_COORD \ |
| ivec2(floor(gl_FragCoord.xy)) |
| #define pixelLocalLoadImpl(NAME_R) \ |
| imageLoad(NAME_R, PIXEL_I_COORD) |
| vec4 barrierAfter(vec4 expressionResult) |
| { |
| memoryBarrier(); |
| return expressionResult; |
| } |
| ivec4 barrierAfter(ivec4 expressionResult) |
| { |
| memoryBarrier(); |
| return expressionResult; |
| } |
| uvec4 barrierAfter(uvec4 expressionResult) |
| { |
| memoryBarrier(); |
| return expressionResult; |
| } |
| #define pixelLocalStoreImpl(NAME_W, VALUE_EXPRESSION) \ |
| { \ |
| imageStore(NAME_W, PIXEL_I_COORD, barrierAfter(VALUE_EXPRESSION)); \ |
| memoryBarrier(); \ |
| } |
| // Don't execute pixelLocalStore when depth/stencil fails. |
| layout(early_fragment_tests) in; |
| )"; |
| |
| class PixelLocalStoragePrototype |
| { |
| public: |
| void framebufferPixelLocalStorage(GLuint unit, |
| GLuint backingtexture, |
| GLint level, |
| GLint layer, |
| GLint width, |
| GLint height, |
| GLenum internalformat); |
| void framebufferPixelLocalClearValuefv(GLuint unit, const float *value); |
| void framebufferPixelLocalClearValueiv(GLuint unit, const GLint *value); |
| void framebufferPixelLocalClearValueuiv(GLuint unit, const GLuint *value); |
| void beginPixelLocalStorage(GLsizei n, const GLenum *loadOps); |
| void pixelLocalStorageBarrier(); |
| void endPixelLocalStorage(); |
| |
| private: |
| class PLSPlane |
| { |
| public: |
| PLSPlane() = default; |
| |
| void reset(GLuint tex, GLsizei width, GLsizei height, GLuint internalformat) |
| { |
| if (mMemoryless && mTex) |
| { |
| glDeleteTextures(1, &mTex); |
| } |
| mMemoryless = !tex; |
| if (mMemoryless) |
| { |
| GLint textureBinding2D; |
| glGetIntegerv(GL_TEXTURE_BINDING_2D, &textureBinding2D); |
| glGenTextures(1, &mTex); |
| glBindTexture(GL_TEXTURE_2D, mTex); |
| glTexStorage2D(GL_TEXTURE_2D, 1, internalformat, width, height); |
| glBindTexture(GL_TEXTURE_2D, textureBinding2D); |
| } |
| else |
| { |
| mTex = tex; |
| } |
| mWidth = width; |
| mHeight = height; |
| mInternalformat = internalformat; |
| } |
| |
| ~PLSPlane() |
| { |
| if (mMemoryless && mTex) |
| { |
| glDeleteTextures(1, &mTex); |
| } |
| } |
| |
| GLuint tex() const { return mTex; } |
| GLsizei width() const { return mWidth; } |
| GLsizei height() const { return mHeight; } |
| GLenum internalformat() const { return mInternalformat; } |
| |
| const float *clearValuef() const { return mClearValuef; } |
| const int32_t *clearValuei() const { return mClearValuei; } |
| const uint32_t *clearValueui() const { return mClearValueui; } |
| |
| void setClearValuef(const float val[4]) { memcpy(mClearValuef, val, sizeof(mClearValuef)); } |
| void setClearValuei(const int32_t val[4]) |
| { |
| memcpy(mClearValuei, val, sizeof(mClearValuei)); |
| } |
| void setClearValueui(const uint32_t val[4]) |
| { |
| memcpy(mClearValueui, val, sizeof(mClearValueui)); |
| } |
| |
| private: |
| PLSPlane &operator=(const PLSPlane &) = delete; |
| PLSPlane(const PLSPlane &) = delete; |
| |
| bool mMemoryless; |
| GLuint mTex = 0; |
| GLsizei mWidth; |
| GLsizei mHeight; |
| GLenum mInternalformat; |
| |
| float mClearValuef[4]{}; |
| int32_t mClearValuei[4]{}; |
| uint32_t mClearValueui[4]{}; |
| }; |
| |
| std::array<PLSPlane, MAX_LOCAL_STORAGE_PLANES> &boundLocalStoragePlanes() |
| { |
| GLint drawFBO; |
| glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFBO); |
| ASSERT(drawFBO != 0); // GL_INVALID_OPERATION! |
| return mPLSPlanes[drawFBO]; |
| } |
| |
| std::map<GLuint, std::array<PLSPlane, MAX_LOCAL_STORAGE_PLANES>> mPLSPlanes; |
| bool mLocalStorageEnabled = false; |
| std::vector<int> mEnabledLocalStoragePlanes; |
| GLint mFramebufferPreviousDefaultWidth = 0; |
| GLint mFramebufferPreviousDefaultHeight = 0; |
| }; |
| |
| // Bootstrap the draft extension assuming an in-scope PixelLocalStoragePrototype object named "pls". |
| #define glFramebufferPixelLocalStorageANGLE pls.framebufferPixelLocalStorage |
| #define glFramebufferPixelLocalClearValuefvANGLE pls.framebufferPixelLocalClearValuefv |
| #define glFramebufferPixelLocalClearValueivANGLE pls.framebufferPixelLocalClearValueiv |
| #define glFramebufferPixelLocalClearValueuivANGLE pls.framebufferPixelLocalClearValueuiv |
| #define glBeginPixelLocalStorageANGLE pls.beginPixelLocalStorage |
| #define glPixelLocalStorageBarrierANGLE pls.pixelLocalStorageBarrier |
| #define glEndPixelLocalStorageANGLE pls.endPixelLocalStorage |
| |
| void PixelLocalStoragePrototype::framebufferPixelLocalStorage(GLuint unit, |
| GLuint backingtexture, |
| GLint level, |
| GLint layer, |
| GLsizei width, |
| GLsizei height, |
| GLenum internalformat) |
| { |
| ASSERT(0 <= unit && unit < MAX_LOCAL_STORAGE_PLANES); // GL_INVALID_VALUE! |
| ASSERT(level == 0); // NOT IMPLEMENTED! |
| ASSERT(layer == 0); // NOT IMPLEMENTED! |
| ASSERT(width > 0 && height > 0); // NOT IMPLEMENTED! |
| boundLocalStoragePlanes()[unit].reset(backingtexture, width, height, internalformat); |
| } |
| |
| void PixelLocalStoragePrototype::framebufferPixelLocalClearValuefv(GLuint unit, |
| const GLfloat *value) |
| { |
| ASSERT(0 <= unit && unit < MAX_LOCAL_STORAGE_PLANES); // GL_INVALID_VALUE! |
| boundLocalStoragePlanes()[unit].setClearValuef(value); |
| } |
| |
| void PixelLocalStoragePrototype::framebufferPixelLocalClearValueiv(GLuint unit, const GLint *value) |
| { |
| ASSERT(0 <= unit && unit < MAX_LOCAL_STORAGE_PLANES); // GL_INVALID_VALUE! |
| boundLocalStoragePlanes()[unit].setClearValuei(value); |
| } |
| |
| void PixelLocalStoragePrototype::framebufferPixelLocalClearValueuiv(GLuint unit, |
| const GLuint *value) |
| { |
| ASSERT(0 <= unit && unit < MAX_LOCAL_STORAGE_PLANES); // GL_INVALID_VALUE! |
| boundLocalStoragePlanes()[unit].setClearValueui(value); |
| } |
| |
| class AutoRestoreDrawBuffers |
| { |
| public: |
| AutoRestoreDrawBuffers() |
| { |
| GLint MAX_COLOR_ATTACHMENTS; |
| glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &MAX_COLOR_ATTACHMENTS); |
| |
| GLint MAX_DRAW_BUFFERS; |
| glGetIntegerv(GL_MAX_DRAW_BUFFERS, &MAX_DRAW_BUFFERS); |
| |
| for (int i = 0; i < MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; ++i) |
| { |
| GLint drawBuffer; |
| glGetIntegerv(GL_DRAW_BUFFER0 + i, &drawBuffer); |
| // glDrawBuffers must not reference an attachment at or beyond |
| // MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE. |
| if (GL_COLOR_ATTACHMENT0 <= drawBuffer && |
| drawBuffer < GL_COLOR_ATTACHMENT0 + MAX_COLOR_ATTACHMENTS) |
| { |
| // GL_INVALID_OPERATION! |
| ASSERT(drawBuffer < GL_COLOR_ATTACHMENT0 + MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE); |
| } |
| mDrawBuffers[i] = drawBuffer; |
| } |
| // glDrawBuffers must be GL_NONE at or beyond MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE. |
| for (int i = MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; i < MAX_DRAW_BUFFERS; ++i) |
| { |
| GLint drawBuffer; |
| glGetIntegerv(GL_DRAW_BUFFER0 + i, &drawBuffer); |
| ASSERT(drawBuffer == GL_NONE); // GL_INVALID_OPERATION! |
| } |
| } |
| |
| ~AutoRestoreDrawBuffers() |
| { |
| glDrawBuffers(MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE, mDrawBuffers); |
| } |
| |
| private: |
| GLenum mDrawBuffers[MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE]; |
| }; |
| |
| class AutoRestoreClearColor |
| { |
| public: |
| AutoRestoreClearColor() { glGetFloatv(GL_COLOR_CLEAR_VALUE, mClearColor); } |
| |
| ~AutoRestoreClearColor() |
| { |
| glClearColor(mClearColor[0], mClearColor[1], mClearColor[2], mClearColor[3]); |
| } |
| |
| private: |
| float mClearColor[4]; |
| }; |
| |
| class AutoDisableScissor |
| { |
| public: |
| AutoDisableScissor() |
| { |
| glGetIntegerv(GL_SCISSOR_TEST, &mScissorTestEnabled); |
| if (mScissorTestEnabled) |
| { |
| glDisable(GL_SCISSOR_TEST); |
| } |
| } |
| |
| ~AutoDisableScissor() |
| { |
| if (mScissorTestEnabled) |
| { |
| glEnable(GL_SCISSOR_TEST); |
| } |
| } |
| |
| private: |
| GLint mScissorTestEnabled; |
| }; |
| |
| void PixelLocalStoragePrototype::beginPixelLocalStorage(GLsizei n, const GLenum *loadOps) |
| { |
| ASSERT(1 <= n && n <= MAX_LOCAL_STORAGE_PLANES); // GL_INVALID_VALUE! |
| ASSERT(!mLocalStorageEnabled); // GL_INVALID_OPERATION! |
| ASSERT(mEnabledLocalStoragePlanes.empty()); |
| |
| mLocalStorageEnabled = true; |
| |
| const auto &planes = boundLocalStoragePlanes(); |
| |
| // A framebuffer must have no attachments at or beyond MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE. |
| GLint MAX_COLOR_ATTACHMENTS; |
| glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &MAX_COLOR_ATTACHMENTS); |
| for (int i = MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; i < MAX_COLOR_ATTACHMENTS; ++i) |
| { |
| GLint type; |
| glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, |
| GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &type); |
| ASSERT(type == GL_NONE); // GL_INVALID_OPERATION! |
| } |
| |
| int framebufferWidth = 0; |
| int framebufferHeight = 0; |
| bool needsClear = false; |
| GLenum attachmentsToClear[MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + MAX_LOCAL_STORAGE_PLANES]; |
| memset(attachmentsToClear, 0, sizeof(attachmentsToClear)); |
| |
| for (int i = 0; i < n; ++i) |
| { |
| GLuint tex = 0; |
| GLenum internalformat = GL_RGBA8; |
| if (loadOps[i] != GL_DISABLED_ANGLE) |
| { |
| ASSERT(planes[i].tex()); // GL_INVALID_FRAMEBUFFER_OPERATION! |
| tex = planes[i].tex(); |
| internalformat = planes[i].internalformat(); |
| |
| // GL_INVALID_FRAMEBUFFER_OPERATION! |
| ASSERT(!framebufferWidth || framebufferWidth == planes[i].width()); |
| ASSERT(!framebufferHeight || framebufferHeight == planes[i].height()); |
| framebufferWidth = planes[i].width(); |
| framebufferHeight = planes[i].height(); |
| |
| mEnabledLocalStoragePlanes.push_back(i); |
| } |
| if (loadOps[i] == GL_ZERO || loadOps[i] == GL_REPLACE) |
| { |
| // Attach all textures that need clearing to the framebuffer. |
| GLenum attachmentPoint = |
| GL_COLOR_ATTACHMENT0 + MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + i; |
| glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentPoint, GL_TEXTURE_2D, tex, 0); |
| // If the GL is bound to a draw framebuffer object, the ith buffer listed in bufs must |
| // be GL_COLOR_ATTACHMENTi or GL_NONE. |
| needsClear = true; |
| attachmentsToClear[MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + i] = attachmentPoint; |
| } |
| // Bind local storage textures to their corresponding image unit. Use GL_READ_WRITE since |
| // this binding will be referenced by two image2Ds -- one readeonly and one writeonly. |
| glBindImageTexture(i, tex, 0, GL_FALSE, 0, GL_READ_WRITE, internalformat); |
| } |
| if (needsClear) |
| { |
| AutoRestoreDrawBuffers autoRestoreDrawBuffers; |
| AutoRestoreClearColor autoRestoreClearColor; |
| AutoDisableScissor autoDisableScissor; |
| glDrawBuffers(MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + n, attachmentsToClear); |
| for (int i = 0; i < n; ++i) |
| { |
| if (loadOps[i] != GL_ZERO && loadOps[i] != GL_REPLACE) |
| { |
| continue; |
| } |
| constexpr static char zero[4][4]{}; |
| switch (planes[i].internalformat()) |
| { |
| case GL_RGBA8: |
| case GL_R32F: |
| case GL_RGBA16F: |
| case GL_RGBA32F: |
| glClearBufferfv(GL_COLOR, MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + i, |
| loadOps[i] == GL_REPLACE |
| ? planes[i].clearValuef() |
| : reinterpret_cast<const float *>(zero)); |
| break; |
| case GL_RGBA8I: |
| case GL_RGBA16I: |
| glClearBufferiv(GL_COLOR, MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + i, |
| loadOps[i] == GL_REPLACE |
| ? planes[i].clearValuei() |
| : reinterpret_cast<const int32_t *>(zero)); |
| break; |
| case GL_RGBA8UI: |
| case GL_R32UI: |
| case GL_RGBA16UI: |
| case GL_RGBA32UI: |
| glClearBufferuiv(GL_COLOR, MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + i, |
| loadOps[i] == GL_REPLACE |
| ? planes[i].clearValueui() |
| : reinterpret_cast<const uint32_t *>(zero)); |
| break; |
| default: |
| // Internal error. Invalid internalformats should not have made it this far. |
| ASSERT(false); |
| } |
| } |
| // Detach the textures that needed clearing. |
| for (int i = 0; i < n; ++i) |
| { |
| if (loadOps[i] == GL_ZERO || loadOps[i] == GL_REPLACE) |
| { |
| glFramebufferTexture2D( |
| GL_FRAMEBUFFER, |
| GL_COLOR_ATTACHMENT0 + MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE + i, |
| GL_TEXTURE_2D, 0, 0); |
| } |
| } |
| } |
| |
| glGetFramebufferParameteriv(GL_DRAW_FRAMEBUFFER, GL_FRAMEBUFFER_DEFAULT_WIDTH, |
| &mFramebufferPreviousDefaultWidth); |
| glGetFramebufferParameteriv(GL_DRAW_FRAMEBUFFER, GL_FRAMEBUFFER_DEFAULT_HEIGHT, |
| &mFramebufferPreviousDefaultHeight); |
| glFramebufferParameteri(GL_DRAW_FRAMEBUFFER, GL_FRAMEBUFFER_DEFAULT_WIDTH, framebufferWidth); |
| glFramebufferParameteri(GL_DRAW_FRAMEBUFFER, GL_FRAMEBUFFER_DEFAULT_HEIGHT, framebufferHeight); |
| |
| // Do *ALL* barriers since we don't know what the client did with memory before this point. |
| glMemoryBarrier(GL_ALL_BARRIER_BITS); |
| } |
| |
| void PixelLocalStoragePrototype::pixelLocalStorageBarrier() |
| { |
| // In an ideal world we would only need GL_SHADER_IMAGE_ACCESS_BARRIER_BIT, but some drivers |
| // need a bit more persuasion to get this right. |
| glMemoryBarrier(GL_ALL_BARRIER_BITS); |
| } |
| |
| void PixelLocalStoragePrototype::endPixelLocalStorage() |
| { |
| ASSERT(mLocalStorageEnabled); // GL_INVALID_OPERATION! |
| |
| // Do *ALL* barriers since we don't know what the client will do with memory after this point. |
| glMemoryBarrier(GL_ALL_BARRIER_BITS); |
| |
| // Restore framebuffer default dimensions. |
| glFramebufferParameteri(GL_DRAW_FRAMEBUFFER, GL_FRAMEBUFFER_DEFAULT_WIDTH, |
| mFramebufferPreviousDefaultWidth); |
| glFramebufferParameteri(GL_DRAW_FRAMEBUFFER, GL_FRAMEBUFFER_DEFAULT_HEIGHT, |
| mFramebufferPreviousDefaultHeight); |
| |
| // Unbind local storage image textures. |
| for (int i : mEnabledLocalStoragePlanes) |
| { |
| glBindImageTexture(i, 0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8); |
| } |
| mEnabledLocalStoragePlanes.clear(); |
| |
| mLocalStorageEnabled = false; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| constexpr static int W = 128, H = 128; |
| constexpr static std::array<float, 4> FULLSCREEN = {0, 0, W, H}; |
| |
| template <typename T> |
| struct Array |
| { |
| Array(const std::initializer_list<T> &list) : mVec(list) {} |
| operator const T *() const { return mVec.data(); } |
| std::vector<T> mVec; |
| }; |
| template <typename T> |
| static Array<T> MakeArray(const std::initializer_list<T> &list) |
| { |
| return Array<T>(list); |
| } |
| static Array<GLenum> GLenumArray(const std::initializer_list<GLenum> &list) |
| { |
| return Array<GLenum>(list); |
| } |
| |
| class PLSTestTexture |
| { |
| public: |
| PLSTestTexture(GLenum internalformat) { reset(internalformat); } |
| PLSTestTexture(GLenum internalformat, int w, int h) { reset(internalformat, w, h); } |
| PLSTestTexture(PLSTestTexture &&that) : mID(std::exchange(that.mID, 0)) {} |
| void reset(GLenum internalformat) { reset(internalformat, W, H); } |
| void reset(GLenum internalformat, int w, int h) |
| { |
| GLuint id; |
| glGenTextures(1, &id); |
| glBindTexture(GL_TEXTURE_2D, id); |
| glTexStorage2D(GL_TEXTURE_2D, 1, internalformat, w, h); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| if (mID) |
| { |
| glDeleteTextures(1, &mID); |
| } |
| mID = id; |
| } |
| ~PLSTestTexture() |
| { |
| if (mID) |
| { |
| glDeleteTextures(1, &mID); |
| } |
| } |
| operator GLuint() const { return mID; } |
| |
| private: |
| PLSTestTexture &operator=(const PLSTestTexture &) = delete; |
| PLSTestTexture(const PLSTestTexture &) = delete; |
| GLuint mID = 0; |
| }; |
| |
| class PixelLocalStorageTest : public ANGLETest<> |
| { |
| public: |
| PixelLocalStorageTest() |
| { |
| setWindowWidth(W); |
| setWindowHeight(H); |
| setConfigRedBits(8); |
| setConfigGreenBits(8); |
| setConfigBlueBits(8); |
| setConfigAlphaBits(8); |
| } |
| |
| ~PixelLocalStorageTest() |
| { |
| if (mScratchFBO) |
| { |
| glDeleteFramebuffers(1, &mScratchFBO); |
| } |
| } |
| |
| bool supportsPixelLocalStorage() |
| { |
| ASSERT(getClientMajorVersion() == 3); |
| ASSERT(getClientMinorVersion() == 1); |
| |
| if (isD3D11Renderer()) |
| { |
| // We can't implement pixel local storage via shader images on top of D3D11: |
| // |
| // * D3D UAVs don't support aliasing: https://anglebug.com/3032 |
| // * But ES 3.1 doesn't allow most image2D formats to be readwrite |
| // * And we can't use texelFetch because ps_5_0 does not support thread |
| // synchronization operations in shaders (aka memoryBarrier()). |
| // |
| // We will need to do a custom local storage implementation in D3D11 that uses |
| // RWTexture2D<> or, more ideally, the coherent RasterizerOrderedTexture2D<>. |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool supportsPixelLocalStorageCoherent() |
| { |
| return false; // ES 3.1 shader images can't be coherent. |
| } |
| |
| // anglebug.com/7398: imageLoad() eventually starts failing. A workaround is to delete and |
| // recreate the texture every once in a while. Hopefully this goes away once we start using |
| // proper readwrite desktop GL shader images and INTEL_fragment_shader_ordering. |
| bool hasImageLoadBug() { return IsWindows() && IsIntel() && IsOpenGL(); } |
| |
| void useProgram(std::string fsMain) |
| { |
| // Replace: PIXEL_LOCAL_DECL(name, ...) -> PIXEL_LOCAL_DECL_IMPL(name_R, name_W, ...) |
| static std::regex kDeclRegex("(PIXEL_LOCAL_DECL[_UI]*)\\s*\\(\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); |
| fsMain = std::regex_replace(fsMain, kDeclRegex, "$1_IMPL($2_R, $2_W"); |
| |
| // Replace: pixelLocalLoad(name) -> pixelLocalLoadImpl(name_R) |
| static std::regex kLoadRegex("pixelLocalLoad\\s*\\(\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); |
| fsMain = std::regex_replace(fsMain, kLoadRegex, "pixelLocalLoadImpl($1_R"); |
| |
| // Replace: pixelLocalStore(name, ...) -> pixelLocalStoreImpl(name_W, ...) |
| static std::regex kStoreRegex("pixelLocalStore\\s*\\(\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); |
| fsMain = std::regex_replace(fsMain, kStoreRegex, "pixelLocalStoreImpl($1_W"); |
| |
| if (mLTRBLocation >= 0) |
| { |
| glDisableVertexAttribArray(mLTRBLocation); |
| } |
| if (mRGBALocation >= 0) |
| { |
| glDisableVertexAttribArray(mRGBALocation); |
| } |
| if (mAux1Location >= 0) |
| { |
| glDisableVertexAttribArray(mAux1Location); |
| } |
| if (mAux2Location >= 0) |
| { |
| glDisableVertexAttribArray(mAux2Location); |
| } |
| |
| mProgram.makeRaster( |
| R"(#version 310 es |
| precision highp float; |
| |
| uniform float W, H; |
| in vec4 rect; |
| in vec4 incolor; |
| in vec4 inaux1; |
| in vec4 inaux2; |
| out vec4 color; |
| out vec4 aux1; |
| out vec4 aux2; |
| |
| void main() |
| { |
| color = incolor; |
| aux1 = inaux1; |
| aux2 = inaux2; |
| gl_Position.x = ((gl_VertexID & 1) == 0 ? rect.x : rect.z) * 2.0/W - 1.0; |
| gl_Position.y = ((gl_VertexID & 2) == 0 ? rect.y : rect.w) * 2.0/H - 1.0; |
| gl_Position.zw = vec2(0, 1); |
| })", |
| |
| std::string(R"(#version 310 es |
| precision highp float; |
| in vec4 color; |
| in vec4 aux1; |
| in vec4 aux2;)") |
| .append(kLocalStorageGLSLDefines) |
| .append(fsMain) |
| .c_str()); |
| |
| ASSERT_TRUE(mProgram.valid()); |
| |
| glUseProgram(mProgram); |
| |
| glUniform1f(glGetUniformLocation(mProgram, "W"), W); |
| glUniform1f(glGetUniformLocation(mProgram, "H"), H); |
| |
| mLTRBLocation = glGetAttribLocation(mProgram, "rect"); |
| glEnableVertexAttribArray(mLTRBLocation); |
| glVertexAttribDivisor(mLTRBLocation, 1); |
| |
| mRGBALocation = glGetAttribLocation(mProgram, "incolor"); |
| glEnableVertexAttribArray(mRGBALocation); |
| glVertexAttribDivisor(mRGBALocation, 1); |
| |
| mAux1Location = glGetAttribLocation(mProgram, "inaux1"); |
| glEnableVertexAttribArray(mAux1Location); |
| glVertexAttribDivisor(mAux1Location, 1); |
| |
| mAux2Location = glGetAttribLocation(mProgram, "inaux2"); |
| glEnableVertexAttribArray(mAux2Location); |
| glVertexAttribDivisor(mAux2Location, 1); |
| } |
| |
| struct Box |
| { |
| using float4 = std::array<float, 4>; |
| constexpr Box(float4 rect) : rect(rect), color{}, aux1{}, aux2{} {} |
| constexpr Box(float4 rect, float4 incolor) : rect(rect), color(incolor), aux1{}, aux2{} {} |
| constexpr Box(float4 rect, float4 incolor, float4 inaux1) |
| : rect(rect), color(incolor), aux1(inaux1), aux2{} |
| {} |
| constexpr Box(float4 rect, float4 incolor, float4 inaux1, float4 inaux2) |
| : rect(rect), color(incolor), aux1(inaux1), aux2(inaux2) |
| {} |
| float4 rect; |
| float4 color; |
| float4 aux1; |
| float4 aux2; |
| }; |
| |
| enum class UseBarriers : bool |
| { |
| No = false, |
| IfNotCoherent |
| }; |
| |
| void drawBoxes(PixelLocalStoragePrototype &pls, |
| std::vector<Box> boxes, |
| UseBarriers useBarriers = UseBarriers::IfNotCoherent) |
| { |
| if (!supportsPixelLocalStorageCoherent() && useBarriers == UseBarriers::IfNotCoherent) |
| { |
| for (const auto &box : boxes) |
| { |
| glVertexAttribPointer(mLTRBLocation, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| box.rect.data()); |
| glVertexAttribPointer(mRGBALocation, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| box.color.data()); |
| glVertexAttribPointer(mAux1Location, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| box.aux1.data()); |
| glVertexAttribPointer(mAux2Location, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| box.aux2.data()); |
| glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, 1); |
| glPixelLocalStorageBarrierANGLE(); |
| } |
| } |
| else |
| { |
| glVertexAttribPointer(mLTRBLocation, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| boxes[0].rect.data()); |
| glVertexAttribPointer(mRGBALocation, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| boxes[0].color.data()); |
| glVertexAttribPointer(mAux1Location, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| boxes[0].aux1.data()); |
| glVertexAttribPointer(mAux2Location, 4, GL_FLOAT, GL_FALSE, sizeof(Box), |
| boxes[0].aux2.data()); |
| glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, boxes.size()); |
| } |
| } |
| |
| void attachTextureToScratchFBO(GLuint tex) |
| { |
| if (!mScratchFBO) |
| { |
| glGenFramebuffers(1, &mScratchFBO); |
| } |
| glBindFramebuffer(GL_FRAMEBUFFER, mScratchFBO); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); |
| ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); |
| } |
| |
| // Access texture contents by rendering them into FBO 0, rather than just grabbing them with |
| // glReadPixels. |
| void renderTextureToDefaultFramebuffer(GLuint tex) |
| { |
| glBindFramebuffer(GL_FRAMEBUFFER, 0); |
| // Reset the framebuffer contents to some value that might help debugging. |
| glClearColor(.1f, .4f, .6f, .9f); |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| GLint linked = 0; |
| glGetProgramiv(mRenderTextureProgram, GL_LINK_STATUS, &linked); |
| if (!linked) |
| { |
| constexpr static const char *kVS = |
| R"(#version 310 es |
| precision highp float; |
| out vec2 texcoord; |
| void main() |
| { |
| texcoord.x = (gl_VertexID & 1) == 0 ? 0.0 : 1.0; |
| texcoord.y = (gl_VertexID & 2) == 0 ? 0.0 : 1.0; |
| gl_Position = vec4(texcoord * 2.0 - 1.0, 0, 1); |
| })"; |
| |
| constexpr static const char *kFS = |
| R"(#version 310 es |
| precision highp float; |
| uniform highp sampler2D tex; // FIXME! layout(binding=0) causes an ANGLE crash! |
| in vec2 texcoord; |
| out vec4 fragcolor; |
| void main() |
| { |
| fragcolor = texture(tex, texcoord); |
| })"; |
| |
| mRenderTextureProgram.makeRaster(kVS, kFS); |
| ASSERT_TRUE(mRenderTextureProgram.valid()); |
| glUseProgram(mRenderTextureProgram); |
| glUniform1i(glGetUniformLocation(mRenderTextureProgram, "tex"), 0); |
| } |
| |
| glUseProgram(mRenderTextureProgram); |
| glBindTexture(GL_TEXTURE_2D, tex); |
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); |
| } |
| |
| GLProgram mProgram; |
| GLint mLTRBLocation = -1; |
| GLint mRGBALocation = -1; |
| GLint mAux1Location = -1; |
| GLint mAux2Location = -1; |
| |
| GLuint mScratchFBO = 0; |
| GLProgram mRenderTextureProgram; |
| }; |
| |
| // Verify that values from separate draw calls persist in pixel local storage, for all supported |
| // formats. Also verify that clear-to-zero works on every supported format. |
| TEST_P(PixelLocalStorageTest, AllFormats) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| { |
| PixelLocalStoragePrototype pls; |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(plane1, binding=0, rgba8); |
| PIXEL_LOCAL_DECL_I(plane2, binding=1, rgba8i); |
| PIXEL_LOCAL_DECL_UI(plane3, binding=2, rgba8ui); |
| void main() |
| { |
| pixelLocalStore(plane1, color + pixelLocalLoad(plane1)); |
| pixelLocalStore(plane2, ivec4(aux1) + pixelLocalLoad(plane2)); |
| pixelLocalStore(plane3, uvec4(aux2) + pixelLocalLoad(plane3)); |
| })"); |
| |
| PLSTestTexture tex1(GL_RGBA8); |
| PLSTestTexture tex2(GL_RGBA8I); |
| PLSTestTexture tex3(GL_RGBA8UI); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, tex1, 0, 0, W, H, GL_RGBA8); |
| glFramebufferPixelLocalStorageANGLE(1, tex2, 0, 0, W, H, GL_RGBA8I); |
| glFramebufferPixelLocalStorageANGLE(2, tex3, 0, 0, W, H, GL_RGBA8UI); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| glBeginPixelLocalStorageANGLE(3, GLenumArray({GL_ZERO, GL_ZERO, GL_ZERO})); |
| |
| // Accumulate R, G, B, A in 4 separate passes. |
| drawBoxes(pls, {{FULLSCREEN, {1, 0, 0, 0}, {-5, 0, 0, 0}, {1, 0, 0, 0}}, |
| {FULLSCREEN, {0, 1, 0, 0}, {0, -100, 0, 0}, {0, 50, 0, 0}}, |
| {FULLSCREEN, {0, 0, 1, 0}, {0, 0, -70, 0}, {0, 0, 100, 0}}, |
| {FULLSCREEN, {0, 0, 0, 0}, {0, 0, 0, 22}, {0, 0, 0, 255}}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex1); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(255, 255, 255, 0)); |
| |
| attachTextureToScratchFBO(tex2); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(-5, -100, -70, 22)); |
| |
| attachTextureToScratchFBO(tex3); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(1, 50, 100, 255)); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| { |
| PixelLocalStoragePrototype pls; |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(plane1, binding=0, r32f); |
| PIXEL_LOCAL_DECL_UI(plane2, binding=1, r32ui); |
| void main() |
| { |
| pixelLocalStore(plane1, color + pixelLocalLoad(plane1)); |
| pixelLocalStore(plane2, uvec4(aux1) + pixelLocalLoad(plane2)); |
| })"); |
| |
| PLSTestTexture tex1(GL_R32F); |
| PLSTestTexture tex2(GL_R32UI); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, tex1, 0, 0, W, H, GL_R32F); |
| glFramebufferPixelLocalStorageANGLE(1, tex2, 0, 0, W, H, GL_R32UI); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_ZERO, GL_ZERO})); |
| |
| // Accumulate R in 4 separate passes. |
| drawBoxes(pls, {{FULLSCREEN, {-1.5, 0, 0, 0}, {5, 0, 0, 0}}, |
| {FULLSCREEN, {-10.25, 0, 0, 0}, {60, 0, 0, 0}}, |
| {FULLSCREEN, {-100, 0, 0, 0}, {700, 0, 0, 0}}, |
| {FULLSCREEN, {.25, 0, 0, 0}, {8000, 0, 0, 22}}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| // These values should be exact matches. |
| // |
| // GL_R32F is spec'd as a 32-bit IEEE float, and GL_R32UI is a 32-bit unsigned integer. |
| // There is some affordance for fp32 fused operations, but "a + b" is required to be |
| // correctly rounded. |
| // |
| // From the GLSL ES 3.0 spec: |
| // |
| // "Highp unsigned integers have exactly 32 bits of precision. Highp signed integers use |
| // 32 bits, including a sign bit, in two's complement form." |
| // |
| // "Highp floating-point variables within a shader are encoded according to the IEEE 754 |
| // specification for single-precision floating-point values (logically, not necessarily |
| // physically)." |
| // |
| // "Operation: a + b, a - b, a * b |
| // Precision: Correctly rounded." |
| attachTextureToScratchFBO(tex1); |
| EXPECT_PIXEL_RECT32F_EQ(0, 0, W, H, GLColor32F(-111.5, 0, 0, 1)); |
| |
| attachTextureToScratchFBO(tex2); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(8765, 0, 0, 1)); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| { |
| PixelLocalStoragePrototype pls; |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(plane1, binding=0, rgba16f); |
| PIXEL_LOCAL_DECL_I(plane2, binding=1, rgba16i); |
| PIXEL_LOCAL_DECL_UI(plane3, binding=2, rgba16ui); |
| void main() |
| { |
| pixelLocalStore(plane1, color + pixelLocalLoad(plane1)); |
| pixelLocalStore(plane2, ivec4(aux1) + pixelLocalLoad(plane2)); |
| pixelLocalStore(plane3, uvec4(aux2) + pixelLocalLoad(plane3)); |
| })"); |
| |
| PLSTestTexture tex1(GL_RGBA16F); |
| PLSTestTexture tex2(GL_RGBA16I); |
| PLSTestTexture tex3(GL_RGBA16UI); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, tex1, 0, 0, W, H, GL_RGBA16F); |
| glFramebufferPixelLocalStorageANGLE(1, tex2, 0, 0, W, H, GL_RGBA16I); |
| glFramebufferPixelLocalStorageANGLE(2, tex3, 0, 0, W, H, GL_RGBA16UI); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| glBeginPixelLocalStorageANGLE(3, GLenumArray({GL_ZERO, GL_ZERO, GL_ZERO})); |
| |
| // Accumulate R, G, B, A in 4 separate passes. |
| drawBoxes(pls, {{FULLSCREEN, {-100.5, 0, 0, 0}, {-500, 0, 0, 0}, {1, 0, 0, 0}}, |
| {FULLSCREEN, {0, 1024, 0, 0}, {0, -10000, 0, 0}, {0, 500, 0, 0}}, |
| {FULLSCREEN, {0, 0, -4096, 0}, {0, 0, -7000, 0}, {0, 0, 10000, 0}}, |
| {FULLSCREEN, {0, 0, 0, 16384}, {0, 0, 0, 2200}, {0, 0, 0, 65535}}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex1); |
| EXPECT_PIXEL_RECT32F_EQ(0, 0, W, H, GLColor32F(-100.5, 1024, -4096, 16384)); |
| |
| attachTextureToScratchFBO(tex2); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(-500, -10000, -7000, 2200)); |
| |
| attachTextureToScratchFBO(tex3); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(1, 500, 10000, 65535)); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| { |
| PixelLocalStoragePrototype pls; |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(plane1, binding=0, rgba32f); |
| PIXEL_LOCAL_DECL_UI(plane2, binding=1, rgba32ui); |
| void main() |
| { |
| pixelLocalStore(plane1, color + pixelLocalLoad(plane1)); |
| pixelLocalStore(plane2, uvec4(aux1) + pixelLocalLoad(plane2)); |
| })"); |
| |
| PLSTestTexture tex1(GL_RGBA32F); |
| PLSTestTexture tex2(GL_RGBA32UI); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, tex1, 0, 0, W, H, GL_RGBA32F); |
| glFramebufferPixelLocalStorageANGLE(1, tex2, 0, 0, W, H, GL_RGBA32UI); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_ZERO, GL_ZERO})); |
| |
| // Accumulate R, G, B, A in 4 separate passes. |
| drawBoxes(pls, {{FULLSCREEN, {-100.5, 0, 0, 0}, {1, 0, 0, 0}}, |
| {FULLSCREEN, {0, 1024, 0, 0}, {0, 500, 0, 0}}, |
| {FULLSCREEN, {0, 0, -4096, 0}, {0, 0, 10000, 0}}, |
| {FULLSCREEN, {0, 0, 0, 16384}, {0, 0, 0, 65535}}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex1); |
| EXPECT_PIXEL_RECT32F_EQ(0, 0, W, H, GLColor32F(-100.5, 1024, -4096, 16384)); |
| |
| attachTextureToScratchFBO(tex2); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(1, 500, 10000, 65535)); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| } |
| |
| // Check proper functioning of glFramebufferPixelLocalClearValue{fi ui}vANGLE. |
| TEST_P(PixelLocalStorageTest, ClearValue) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| // Scissor and clear color should not affect clear loads. |
| glEnable(GL_SCISSOR_TEST); |
| glScissor(0, 0, 1, 1); |
| glClearColor(.1f, .2f, .3f, .4f); |
| |
| PLSTestTexture texf(GL_RGBA8); |
| PLSTestTexture texi(GL_RGBA16I); |
| PLSTestTexture texui(GL_RGBA16UI); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, texf, 0, 0, W, H, GL_RGBA8); |
| glFramebufferPixelLocalStorageANGLE(1, texi, 0, 0, W, H, GL_RGBA16I); |
| glFramebufferPixelLocalStorageANGLE(2, texui, 0, 0, W, H, GL_RGBA16UI); |
| auto clearLoads = GLenumArray({GL_REPLACE, GL_REPLACE, GL_REPLACE}); |
| |
| // Clear values are initially zero. |
| glBeginPixelLocalStorageANGLE(3, clearLoads); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0, 0, 0, 0)); |
| |
| // Test custom clear values. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalClearValuefvANGLE(0, MakeArray<float>({1, 0, 0, 0})); |
| glFramebufferPixelLocalClearValueivANGLE(1, MakeArray<int32_t>({1, 2, 3, 4})); |
| glFramebufferPixelLocalClearValueuivANGLE(2, MakeArray<uint32_t>({5, 6, 7, 8})); |
| glBeginPixelLocalStorageANGLE(3, clearLoads); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(255, 0, 0, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(1, 2, 3, 4)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(5, 6, 7, 8)); |
| |
| // Different clear value types are separate state values. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(1, texf, 0, 0, W, H, GL_RGBA8); |
| glFramebufferPixelLocalStorageANGLE(2, texi, 0, 0, W, H, GL_RGBA16I); |
| glFramebufferPixelLocalStorageANGLE(0, texui, 0, 0, W, H, GL_RGBA16UI); |
| glBeginPixelLocalStorageANGLE(3, clearLoads); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0, 0, 0, 0)); |
| |
| // Set new custom clear values. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalClearValuefvANGLE(1, MakeArray<float>({0, 1, 0, 0})); |
| glFramebufferPixelLocalClearValueivANGLE(2, MakeArray<int32_t>({100, 200, 300, 400})); |
| glFramebufferPixelLocalClearValueuivANGLE(0, MakeArray<uint32_t>({500, 600, 700, 800})); |
| glBeginPixelLocalStorageANGLE(3, clearLoads); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 255, 0, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(100, 200, 300, 400)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(500, 600, 700, 800)); |
| |
| // Different clear value types are separate state values (final rotation). |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(2, texf, 0, 0, W, H, GL_RGBA8); |
| glFramebufferPixelLocalStorageANGLE(0, texi, 0, 0, W, H, GL_RGBA16I); |
| glFramebufferPixelLocalStorageANGLE(1, texui, 0, 0, W, H, GL_RGBA16UI); |
| glBeginPixelLocalStorageANGLE(3, clearLoads); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0, 0, 0, 0)); |
| |
| // Set new custom clear values (final rotation). |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalClearValuefvANGLE(2, MakeArray<float>({0, 0, 1, 0})); |
| glFramebufferPixelLocalClearValueivANGLE(0, MakeArray<int32_t>({1000, 2000, 3000, 4000})); |
| glFramebufferPixelLocalClearValueuivANGLE(1, MakeArray<uint32_t>({5000, 6000, 7000, 8000})); |
| glBeginPixelLocalStorageANGLE(3, clearLoads); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 0, 255, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(1000, 2000, 3000, 4000)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(5000, 6000, 7000, 8000)); |
| |
| // GL_ZERO shouldn't be affected by the clear color state. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glBeginPixelLocalStorageANGLE(3, GLenumArray({GL_ZERO, GL_ZERO, GL_ZERO})); |
| glEndPixelLocalStorageANGLE(); |
| attachTextureToScratchFBO(texf); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texi); |
| EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(0, 0, 0, 0)); |
| attachTextureToScratchFBO(texui); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0, 0, 0, 0)); |
| } |
| |
| // Check proper support of GL_ZERO, GL_KEEP, GL_REPLACE, and GL_DISABLED_ANGLE loadOps. Also verify |
| // that it works do draw with GL_MAX_LOCAL_STORAGE_PLANES_ANGLE planes. |
| TEST_P(PixelLocalStorageTest, LoadOps) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| std::stringstream fs; |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| fs << "PIXEL_LOCAL_DECL(pls" << i << ", binding=" << i << ", rgba8);\n"; |
| } |
| fs << "void main() {\n"; |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| fs << "pixelLocalStore(pls" << i << ", color + pixelLocalLoad(pls" << i << "));\n"; |
| } |
| fs << "}"; |
| useProgram(fs.str().c_str()); |
| |
| // Create pls textures and clear them to red. |
| glClearColor(1, 0, 0, 1); |
| std::vector<PLSTestTexture> texs; |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| texs.emplace_back(GL_RGBA8); |
| attachTextureToScratchFBO(texs[i]); |
| glClear(GL_COLOR_BUFFER_BIT); |
| } |
| |
| // Turn on scissor to try and confuse the local storage clear step. |
| glEnable(GL_SCISSOR_TEST); |
| glScissor(0, 0, 20, H); |
| |
| // Set up pls color planes with a clear color of black. Odd units load with GL_REPLACE (cleared |
| // to black) and even load with GL_KEEP (preserved red). |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| std::vector<GLenum> loadOps(MAX_LOCAL_STORAGE_PLANES); |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| glFramebufferPixelLocalClearValuefvANGLE(i, MakeArray<float>({0, 0, 0, 1})); |
| glFramebufferPixelLocalStorageANGLE(i, texs[i], 0, 0, W, H, GL_RGBA8); |
| loadOps[i] = (i & 1) ? GL_REPLACE : GL_KEEP; |
| } |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| // Draw transparent green into all pls attachments. |
| glBeginPixelLocalStorageANGLE(MAX_LOCAL_STORAGE_PLANES, loadOps.data()); |
| drawBoxes(pls, {{{FULLSCREEN}, {0, 1, 0, 0}}}); |
| glEndPixelLocalStorageANGLE(); |
| |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texs[i], 0); |
| // Check that the draw buffers didn't get perturbed by local storage -- GL_COLOR_ATTACHMENT0 |
| // is currently off, so glClear has no effect. This also verifies that local storage planes |
| // didn't get left attached to the framebuffer somewhere with draw buffers on. |
| glClear(GL_COLOR_BUFFER_BIT); |
| EXPECT_PIXEL_RECT_EQ(0, 0, 20, H, |
| loadOps[i] == GL_REPLACE ? GLColor(0, 255, 0, 255) |
| : /*GL_KEEP*/ GLColor(255, 255, 0, 255)); |
| // Check that the scissor didn't get perturbed by local storage. |
| EXPECT_PIXEL_RECT_EQ( |
| 20, 0, W - 20, H, |
| loadOps[i] == GL_REPLACE ? GLColor(0, 0, 0, 255) : /*GL_KEEP*/ GLColor(255, 0, 0, 255)); |
| } |
| |
| // Detach the last read pls texture from the framebuffer. |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); |
| |
| // Now test GL_DISABLED_ANGLE and GL_ZERO. |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| loadOps[i] = loadOps[i] == GL_REPLACE ? GL_ZERO : GL_DISABLED_ANGLE; |
| } |
| |
| // Execute a pls pass without a draw. |
| glBeginPixelLocalStorageANGLE(MAX_LOCAL_STORAGE_PLANES, loadOps.data()); |
| glEndPixelLocalStorageANGLE(); |
| |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texs[i], 0); |
| if (loadOps[i] == GL_ZERO) |
| { |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(0, 0, 0, 0)); |
| } |
| else |
| { |
| EXPECT_PIXEL_RECT_EQ(0, 0, 20, H, GLColor(255, 255, 0, 255)); |
| EXPECT_PIXEL_RECT_EQ(20, 0, W - 20, H, GLColor(255, 0, 0, 255)); |
| } |
| } |
| |
| // Now turn GL_COLOR_ATTACHMENT0 back on and check that the clear color and scissor didn't get |
| // perturbed by local storage. |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texs[1], 0); |
| glDrawBuffers(1, std::array<GLenum, 1>{GL_COLOR_ATTACHMENT0}.data()); |
| glClear(GL_COLOR_BUFFER_BIT); |
| EXPECT_PIXEL_RECT_EQ(0, 0, 20, H, GLColor(255, 0, 0, 255)); |
| EXPECT_PIXEL_RECT_EQ(20, 0, W - 20, H, GLColor(0, 0, 0, 0)); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // This next series of tests checks that GL utilities for rejecting fragments prevent stores to PLS: |
| // |
| // * discard |
| // * return (from main) |
| // * stencil test |
| // * depth test |
| // * viewport |
| // |
| // Some utilities are not legal in ANGLE_shader_pixel_local_storage: |
| // |
| // * gl_SampleMask is disallowed by the spec |
| // * discard, after potential calls to pixelLocalLoad/Store, is disallowed by the spec |
| // * pixelLocalLoad/Store after a return from main is disallowed by the spec |
| // |
| // To run the tests, bind a FragmentRejectTestFBO and draw {FRAG_REJECT_TEST_BOX}: |
| // |
| // * {0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT} should be green |
| // * Fragments outside should have been rejected, leaving the pixels black |
| // |
| struct FragmentRejectTestFBO : GLFramebuffer |
| { |
| FragmentRejectTestFBO(PixelLocalStoragePrototype &pls, GLuint tex) |
| { |
| glBindFramebuffer(GL_FRAMEBUFFER, *this); |
| glFramebufferPixelLocalStorageANGLE(0, tex, 0, 0, W, H, GL_RGBA8); |
| glFramebufferPixelLocalClearValuefvANGLE(0, MakeArray<float>({0, 0, 0, 1})); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| } |
| }; |
| constexpr static int FRAG_REJECT_TEST_WIDTH = 64; |
| constexpr static int FRAG_REJECT_TEST_HEIGHT = 64; |
| constexpr static PixelLocalStorageTest::Box FRAG_REJECT_TEST_BOX( |
| FULLSCREEN, |
| {0, 1, 0, 0}, // draw color |
| {0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT}); // reject pixels outside aux1 |
| |
| // Check that discard prevents stores to pls. |
| // (discard after pixelLocalLoad/Store is illegal because it would have different behavior with |
| // shader images vs framebuffer fetch.) |
| TEST_P(PixelLocalStorageTest, FragmentReject_discard) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| PLSTestTexture tex(GL_RGBA8); |
| FragmentRejectTestFBO fbo(pls, tex); |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(pls, binding=0, rgba8); |
| void main() |
| { |
| vec4 dst = pixelLocalLoad(pls); |
| if (any(lessThan(gl_FragCoord.xy, aux1.xy)) || any(greaterThan(gl_FragCoord.xy, aux1.zw))) |
| { |
| discard; |
| } |
| pixelLocalStore(pls, color + dst); |
| })"); |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| drawBoxes(pls, {FRAG_REJECT_TEST_BOX}); |
| glEndPixelLocalStorageANGLE(); |
| |
| renderTextureToDefaultFramebuffer(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT, GLColor::green); |
| EXPECT_PIXEL_RECT_EQ(FRAG_REJECT_TEST_WIDTH, 0, W - FRAG_REJECT_TEST_WIDTH, |
| FRAG_REJECT_TEST_HEIGHT, GLColor::black); |
| EXPECT_PIXEL_RECT_EQ(0, FRAG_REJECT_TEST_HEIGHT, W, H - FRAG_REJECT_TEST_HEIGHT, |
| GLColor::black); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that return from main prevents stores to PLS. |
| // (pixelLocalLoad/Store after a return from main is illegal because |
| // GL_ARB_fragment_shader_interlock isn't allowed after a return from main.) |
| TEST_P(PixelLocalStorageTest, FragmentReject_return) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| PLSTestTexture tex(GL_RGBA8); |
| FragmentRejectTestFBO fbo(pls, tex); |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(pls, binding=0, rgba8); |
| void main() |
| { |
| if (any(lessThan(gl_FragCoord.xy, aux1.xy)) || any(greaterThan(gl_FragCoord.xy, aux1.zw))) |
| { |
| return; |
| } |
| pixelLocalStore(pls, color + pixelLocalLoad(pls)); |
| })"); |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| drawBoxes(pls, {FRAG_REJECT_TEST_BOX}); |
| glEndPixelLocalStorageANGLE(); |
| |
| renderTextureToDefaultFramebuffer(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT, GLColor::green); |
| EXPECT_PIXEL_RECT_EQ(FRAG_REJECT_TEST_WIDTH, 0, W - FRAG_REJECT_TEST_WIDTH, |
| FRAG_REJECT_TEST_HEIGHT, GLColor::black); |
| EXPECT_PIXEL_RECT_EQ(0, FRAG_REJECT_TEST_HEIGHT, W, H - FRAG_REJECT_TEST_HEIGHT, |
| GLColor::black); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that the stencil test prevents stores to PLS. |
| TEST_P(PixelLocalStorageTest, FragmentReject_stencil) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| PLSTestTexture tex(GL_RGBA8); |
| FragmentRejectTestFBO fbo(pls, tex); |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(pls, binding=0, rgba8); |
| void main() |
| { |
| pixelLocalStore(pls, color + pixelLocalLoad(pls)); |
| })"); |
| GLuint depthStencil; |
| glGenRenderbuffers(1, &depthStencil); |
| glBindRenderbuffer(GL_RENDERBUFFER, depthStencil); |
| glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, W, H); |
| glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, |
| depthStencil); |
| glClearStencil(0); |
| glClear(GL_STENCIL_BUFFER_BIT); |
| |
| // glStencilFunc(GL_NEVER, ...) should not update pls. |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| glEnable(GL_STENCIL_TEST); |
| glStencilFunc(GL_NEVER, 1, ~0u); |
| glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); |
| drawBoxes(pls, {{{0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT}}}); |
| glEndPixelLocalStorageANGLE(); |
| glDisable(GL_STENCIL_TEST); |
| attachTextureToScratchFBO(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor::black); |
| |
| // Stencil should be preserved after PLS, and only pixels that pass the stencil test should |
| // update PLS next. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| if (hasImageLoadBug()) // anglebug.com/7398 |
| { |
| tex.reset(GL_RGBA8); |
| glFramebufferPixelLocalStorageANGLE(0, tex, 0, 0, W, H, GL_RGBA8); |
| } |
| glEnable(GL_STENCIL_TEST); |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| glStencilFunc(GL_NOTEQUAL, 0, ~0u); |
| glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO); |
| drawBoxes(pls, {FRAG_REJECT_TEST_BOX}); |
| glDisable(GL_STENCIL_TEST); |
| glEndPixelLocalStorageANGLE(); |
| renderTextureToDefaultFramebuffer(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT, GLColor::green); |
| EXPECT_PIXEL_RECT_EQ(FRAG_REJECT_TEST_WIDTH, 0, W - FRAG_REJECT_TEST_WIDTH, |
| FRAG_REJECT_TEST_HEIGHT, GLColor::black); |
| EXPECT_PIXEL_RECT_EQ(0, FRAG_REJECT_TEST_HEIGHT, W, H - FRAG_REJECT_TEST_HEIGHT, |
| GLColor::black); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that the depth test prevents stores to PLS. |
| TEST_P(PixelLocalStorageTest, FragmentReject_depth) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| PLSTestTexture tex(GL_RGBA8); |
| FragmentRejectTestFBO fbo(pls, tex); |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(pls, binding=0, rgba8); |
| void main() |
| { |
| pixelLocalStore(pls, pixelLocalLoad(pls) + color); |
| })"); |
| GLuint depth; |
| glGenRenderbuffers(1, &depth); |
| glBindRenderbuffer(GL_RENDERBUFFER, depth); |
| glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, W, H); |
| glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth); |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| glClearDepthf(0.f); |
| glClear(GL_DEPTH_BUFFER_BIT); |
| glEnable(GL_DEPTH_TEST); |
| glEnable(GL_SCISSOR_TEST); |
| glScissor(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT); |
| glClearDepthf(1.f); |
| glClear(GL_DEPTH_BUFFER_BIT); |
| glDisable(GL_SCISSOR_TEST); |
| glDepthFunc(GL_LESS); |
| drawBoxes(pls, {FRAG_REJECT_TEST_BOX}); |
| glEndPixelLocalStorageANGLE(); |
| glDisable(GL_DEPTH_TEST); |
| |
| renderTextureToDefaultFramebuffer(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT, GLColor::green); |
| EXPECT_PIXEL_RECT_EQ(FRAG_REJECT_TEST_WIDTH, 0, W - FRAG_REJECT_TEST_WIDTH, |
| FRAG_REJECT_TEST_HEIGHT, GLColor::black); |
| EXPECT_PIXEL_RECT_EQ(0, FRAG_REJECT_TEST_HEIGHT, W, H - FRAG_REJECT_TEST_HEIGHT, |
| GLColor::black); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that restricting the viewport also restricts stores to PLS. |
| TEST_P(PixelLocalStorageTest, FragmentReject_viewport) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| PLSTestTexture tex(GL_RGBA8); |
| FragmentRejectTestFBO fbo(pls, tex); |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(pls, binding=0, rgba8); |
| void main() |
| { |
| vec4 dst = pixelLocalLoad(pls); |
| pixelLocalStore(pls, color + dst); |
| })"); |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| glViewport(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT); |
| drawBoxes(pls, {FRAG_REJECT_TEST_BOX}); |
| glEndPixelLocalStorageANGLE(); |
| glViewport(0, 0, W, H); |
| |
| renderTextureToDefaultFramebuffer(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, FRAG_REJECT_TEST_WIDTH, FRAG_REJECT_TEST_HEIGHT, GLColor::green); |
| EXPECT_PIXEL_RECT_EQ(FRAG_REJECT_TEST_WIDTH, 0, W - FRAG_REJECT_TEST_WIDTH, |
| FRAG_REJECT_TEST_HEIGHT, GLColor::black); |
| EXPECT_PIXEL_RECT_EQ(0, FRAG_REJECT_TEST_HEIGHT, W, H - FRAG_REJECT_TEST_HEIGHT, |
| GLColor::black); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that results are only nondeterministic within predictable constraints, and that no data is |
| // random or leaked from other contexts when we forget to insert a barrier. |
| TEST_P(PixelLocalStorageTest, ForgetBarrier) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL_UI(framebuffer, binding=0, r32ui); |
| void main() |
| { |
| uvec4 dst = pixelLocalLoad(framebuffer); |
| pixelLocalStore(framebuffer, uvec4(color) + dst * 2u); |
| })"); |
| |
| // Draw r=100, one pixel at a time, in random order. |
| constexpr static int NUM_PIXELS = H * W; |
| std::vector<Box> boxesA_100; |
| int pixelIdx = 0; |
| for (int i = 0; i < NUM_PIXELS; ++i) |
| { |
| int iy = pixelIdx / W; |
| float y = iy; |
| int ix = pixelIdx % W; |
| float x = ix; |
| pixelIdx = |
| (pixelIdx + 69484171) % NUM_PIXELS; // Prime numbers guarantee we hit every pixel once. |
| boxesA_100.push_back(Box{{x, y, x + 1, y + 1}, {100, 0, 0, 0}}); |
| } |
| |
| // Draw r=7, one pixel at a time, in random order. |
| std::vector<Box> boxesB_7; |
| for (int i = 0; i < NUM_PIXELS; ++i) |
| { |
| int iy = pixelIdx / W; |
| float y = iy; |
| int ix = pixelIdx % W; |
| float x = ix; |
| pixelIdx = |
| (pixelIdx + 97422697) % NUM_PIXELS; // Prime numbers guarantee we hit every pixel once. |
| boxesB_7.push_back(Box{{x, y, x + 1, y + 1}, {7, 0, 0, 0}}); |
| } |
| |
| PLSTestTexture tex(GL_R32UI); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, tex, 0, 0, W, H, GL_R32UI); |
| glFramebufferPixelLocalClearValueuivANGLE(0, MakeArray<uint32_t>({1, 0, 0, 0})); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| // First make sure it works properly with a barrier. |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| drawBoxes(pls, boxesA_100, UseBarriers::No); |
| glPixelLocalStorageBarrierANGLE(); |
| drawBoxes(pls, boxesB_7, UseBarriers::No); |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(211, 0, 0, 1)); |
| |
| ASSERT_GL_NO_ERROR(); |
| |
| // Vulkan generates rightful "SYNC-HAZARD-READ_AFTER_WRITE" validation errors when we omit the |
| // barrier. |
| ANGLE_SKIP_TEST_IF(IsVulkan()); |
| |
| // Now forget to insert the barrier and ensure our nondeterminism still falls within predictable |
| // constraints. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| if (hasImageLoadBug()) // anglebug.com/7398 |
| { |
| tex.reset(GL_R32UI); |
| glFramebufferPixelLocalStorageANGLE(0, tex, 0, 0, W, H, GL_R32UI); |
| } |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_REPLACE})); |
| drawBoxes(pls, boxesA_100, UseBarriers::No); |
| // OOPS! We forgot to insert a barrier! |
| drawBoxes(pls, boxesB_7, UseBarriers::No); |
| glEndPixelLocalStorageANGLE(); |
| |
| uint32_t pixels[H * W * 4]; |
| attachTextureToScratchFBO(tex); |
| glReadPixels(0, 0, W, H, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixels); |
| for (int r = 0; r < NUM_PIXELS * 4; r += 4) |
| { |
| // When two fragments, A and B, touch a pixel, there are 6 possible orderings of operations: |
| // |
| // * Read A, Write A, Read B, Write B |
| // * Read B, Write B, Read A, Write A |
| // * Read A, Read B, Write A, Write B |
| // * Read A, Read B, Write B, Write A |
| // * Read B, Read A, Write B, Write A |
| // * Read B, Read A, Write A, Write B |
| // |
| // Which (assumimg the read and/or write operations themselves are atomic), is equivalent to |
| // 1 of 4 potential effects: |
| bool isAcceptableValue = pixels[r] == 211 || // A, then B ( 7 + (100 + 1 * 2) * 2 == 211) |
| pixels[r] == 118 || // B, then A (100 + ( 7 + 1 * 2) * 2 == 118) |
| pixels[r] == 102 || // A only (100 + 1 * 2 == 102) |
| pixels[r] == 9; |
| if (!isAcceptableValue) |
| { |
| printf(__FILE__ "(%i): UNACCEPTABLE value at pixel location [%i, %i]\n", __LINE__, |
| (r / 4) % W, (r / 4) / W); |
| printf(" Got: %u\n", pixels[r]); |
| printf(" Expected one of: { 211, 118, 102, 9 }\n"); |
| } |
| ASSERT_TRUE(isAcceptableValue); |
| } |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check loading and storing from memoryless local storage planes. |
| TEST_P(PixelLocalStorageTest, MemorylessStorage) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| // Bind the texture, but don't call glTexStorage until after creating the memoryless plane. |
| GLTexture tex; |
| glBindTexture(GL_TEXTURE_2D, tex); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| // Create a memoryless plane. |
| glFramebufferPixelLocalStorageANGLE(1, 0 /*memoryless*/, 0, 0, W, H, GL_RGBA8); |
| // Define the persistent texture now, after attaching the memoryless pixel local storage. This |
| // verifies that the GL_TEXTURE_2D binding doesn't get perturbed by local storage. |
| glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H); |
| glFramebufferPixelLocalStorageANGLE(0, tex, 0, 0, W, H, GL_RGBA8); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_ZERO, GL_ZERO})); |
| |
| // Draw into memoryless storage. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(memoryless, binding=1, rgba8); |
| void main() |
| { |
| pixelLocalStore(memoryless, color + pixelLocalLoad(memoryless)); |
| })"); |
| |
| drawBoxes(pls, {{{0, 20, W, H}, {1, 0, 0, 0}}, |
| {{0, 40, W, H}, {0, 1, 0, 0}}, |
| {{0, 60, W, H}, {0, 0, 1, 0}}}); |
| |
| // Transfer to a texture. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(framebuffer, binding=0, rgba8); |
| PIXEL_LOCAL_DECL(memoryless, binding=1, rgba8); |
| void main() |
| { |
| pixelLocalStore(framebuffer, vec4(1) - pixelLocalLoad(memoryless)); |
| })"); |
| |
| drawBoxes(pls, {{FULLSCREEN}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 60, W, H - 60, GLColor(0, 0, 0, 255)); |
| EXPECT_PIXEL_RECT_EQ(0, 40, W, 20, GLColor(0, 0, 255, 255)); |
| EXPECT_PIXEL_RECT_EQ(0, 20, W, 20, GLColor(0, 255, 255, 255)); |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, 20, GLColor(255, 255, 255, 255)); |
| |
| // Ensure the GL_TEXTURE_2D binding still hasn't been perturbed by local storage. |
| GLint textureBinding2D; |
| glGetIntegerv(GL_TEXTURE_BINDING_2D, &textureBinding2D); |
| ASSERT_EQ((GLuint)textureBinding2D, tex); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that it works to render with the maximum supported data payload: |
| // |
| // GL_MAX_LOCAL_STORAGE_PLANES_ANGLE |
| // GL_MAX_LOCAL_STORAGE_BYTES_ANGLE |
| // GL_MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE_ANGLE |
| // |
| TEST_P(PixelLocalStorageTest, MaxCapacity) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| // Try to use up MAX_LOCAL_STORAGE_BYTES of data. |
| int numRegisters = MAX_LOCAL_STORAGE_BYTES / 4; |
| ASSERT(numRegisters >= |
| MAX_LOCAL_STORAGE_PLANES); // Otherwise MAX_LOCAL_STORAGE_PLANES is impossible. |
| int numExtraRegisters = numRegisters - MAX_LOCAL_STORAGE_PLANES; |
| int num32s = std::min(numExtraRegisters / 3, MAX_LOCAL_STORAGE_PLANES); |
| int num16s = std::min(numExtraRegisters - num32s * 3, MAX_LOCAL_STORAGE_PLANES - num32s); |
| int num8s = MAX_LOCAL_STORAGE_PLANES - num32s - num16s; |
| ASSERT(num8s >= 0); |
| ASSERT(num32s * 4 + num16s * 2 + num8s <= numRegisters); |
| |
| std::stringstream fs; |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| const char *format = i < num32s ? "rgba32ui" |
| : i < (num32s + num16s) ? "rgba16ui" |
| : "rgba8ui"; |
| fs << "PIXEL_LOCAL_DECL_UI(pls" << i << ", binding=" << i << ", " << format << ");\n"; |
| } |
| for (int i = 0; i < MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; ++i) |
| { |
| fs << "out uvec4 out" << i << ";\n"; |
| } |
| fs << "void main() {\n"; |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| fs << "pixelLocalStore(pls" << i << ", uvec4(color) - uvec4(" << i << "));\n"; |
| } |
| for (int i = 0; i < MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; ++i) |
| { |
| fs << "out" << i << " = uvec4(aux1) + uvec4(" << i << ");\n"; |
| } |
| fs << "}"; |
| useProgram(fs.str().c_str()); |
| |
| glViewport(0, 0, W, H); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| std::vector<PLSTestTexture> localTexs; |
| localTexs.reserve(MAX_LOCAL_STORAGE_PLANES); |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| GLenum internalformat = i < num32s ? GL_RGBA32UI |
| : i < (num32s + num16s) ? GL_RGBA16UI |
| : GL_RGBA8UI; |
| localTexs.emplace_back(internalformat); |
| glFramebufferPixelLocalStorageANGLE(i, localTexs[i], 0, 0, W, H, internalformat); |
| } |
| std::vector<PLSTestTexture> renderTexs; |
| renderTexs.reserve(MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE); |
| std::vector<GLenum> drawBuffers(MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE); |
| for (int i = 0; i < MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; ++i) |
| { |
| renderTexs.emplace_back(GL_RGBA32UI); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, |
| renderTexs[i], 0); |
| drawBuffers[i] = GL_COLOR_ATTACHMENT0 + i; |
| } |
| glDrawBuffers(drawBuffers.size(), drawBuffers.data()); |
| |
| glBeginPixelLocalStorageANGLE(MAX_LOCAL_STORAGE_PLANES, |
| std::vector<GLenum>(MAX_LOCAL_STORAGE_PLANES, GL_ZERO).data()); |
| drawBoxes(pls, {{FULLSCREEN, {255, 254, 253, 252}, {0, 1, 2, 3}}}); |
| glEndPixelLocalStorageANGLE(); |
| |
| for (int i = 0; i < MAX_LOCAL_STORAGE_PLANES; ++i) |
| { |
| attachTextureToScratchFBO(localTexs[i]); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(255u - i, 254u - i, 253u - i, 252u - i)); |
| } |
| for (int i = 0; i < MAX_FRAGMENT_OUTPUTS_WITH_LOCAL_STORAGE; ++i) |
| { |
| attachTextureToScratchFBO(renderTexs[i]); |
| EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0u + i, 1u + i, 2u + i, 3u + i)); |
| } |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that the pls is preserved when a shader does not call pixelLocalStore(). (Whether that's |
| // because a conditional branch failed or because the shader didn't write to it at all.) It's |
| // conceivable that an implementation may need to be careful to preserve the pls contents in this |
| // scenario. |
| // |
| // Also check that a pixelLocalLoad() of an r32f texture returns (r, 0, 0, 1). |
| TEST_P(PixelLocalStorageTest, LoadOnly) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| PLSTestTexture tex(GL_RGBA8); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, 0 /*Memoryless*/, 0, 0, W, H, GL_R32F); |
| glFramebufferPixelLocalStorageANGLE(1, tex, 0, 0, W, H, GL_RGBA8); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| // Leave unit 0 with the default clear value of zero. |
| glFramebufferPixelLocalClearValuefvANGLE(1, MakeArray<float>({0, 1, 0, 0})); |
| glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_REPLACE, GL_REPLACE})); |
| |
| // Pass 1: draw to memoryless conditionally. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(memoryless, binding=0, r32f); |
| void main() |
| { |
| // Omit braces on the 'if' to ensure proper insertion of memoryBarriers in the translator. |
| if (gl_FragCoord.x < 64.0) |
| pixelLocalStore(memoryless, vec4(1, -.1, .2, -.3)); // Only stores r. |
| })"); |
| drawBoxes(pls, {{FULLSCREEN}}); |
| |
| // Pass 2: draw to tex conditionally. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(tex, binding=1, rgba8); |
| void main() |
| { |
| // Omit braces on the 'if' to ensure proper insertion of memoryBarriers in the translator. |
| if (gl_FragCoord.y < 64.0) |
| pixelLocalStore(tex, vec4(0, 1, 1, 0)); |
| })"); |
| drawBoxes(pls, {{FULLSCREEN}}); |
| |
| // Pass 3: combine memoryless and tex. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(memoryless, binding=0, r32f); |
| PIXEL_LOCAL_DECL(tex, binding=1, rgba8); |
| void main() |
| { |
| pixelLocalStore(tex, pixelLocalLoad(tex) + pixelLocalLoad(memoryless)); |
| })"); |
| drawBoxes(pls, {{FULLSCREEN}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, 64, 64, GLColor(255, 255, 255, 255)); |
| EXPECT_PIXEL_RECT_EQ(64, 0, W - 64, 64, GLColor(0, 255, 255, 255)); |
| EXPECT_PIXEL_RECT_EQ(0, 64, 64, H - 64, GLColor(255, 255, 0, 255)); |
| EXPECT_PIXEL_RECT_EQ(64, 64, W - 64, H - 64, GLColor(0, 255, 0, 255)); |
| |
| ASSERT_GL_NO_ERROR(); |
| |
| // Now treat "tex" as entirely readonly for an entire local storage render pass. |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| PLSTestTexture rttex(GL_RGBA8); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rttex, 0); |
| glDrawBuffers(1, GLenumArray({GL_COLOR_ATTACHMENT0})); |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_DISABLED_ANGLE, GL_KEEP})); |
| |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(tex, binding=1, rgba8); |
| out vec4 fragcolor; |
| void main() |
| { |
| fragcolor = 1.0 - pixelLocalLoad(tex); |
| })"); |
| drawBoxes(pls, {{FULLSCREEN}}); |
| |
| glEndPixelLocalStorageANGLE(); |
| |
| // Ensure "tex" was properly read in the shader. |
| EXPECT_PIXEL_RECT_EQ(0, 0, 64, 64, GLColor(0, 0, 0, 0)); |
| EXPECT_PIXEL_RECT_EQ(64, 0, W - 64, 64, GLColor(255, 0, 0, 0)); |
| EXPECT_PIXEL_RECT_EQ(0, 64, 64, H - 64, GLColor(0, 0, 255, 0)); |
| EXPECT_PIXEL_RECT_EQ(64, 64, W - 64, H - 64, GLColor(255, 0, 255, 0)); |
| |
| // Ensure "tex" was preserved after the shader. |
| attachTextureToScratchFBO(tex); |
| EXPECT_PIXEL_RECT_EQ(0, 0, 64, 64, GLColor(255, 255, 255, 255)); |
| EXPECT_PIXEL_RECT_EQ(64, 0, W - 64, 64, GLColor(0, 255, 255, 255)); |
| EXPECT_PIXEL_RECT_EQ(0, 64, 64, H - 64, GLColor(255, 255, 0, 255)); |
| EXPECT_PIXEL_RECT_EQ(64, 64, W - 64, H - 64, GLColor(0, 255, 0, 255)); |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| // Check that stores and loads in a single shader invocation are coherent. |
| TEST_P(PixelLocalStorageTest, CoherentStoreLoad) |
| { |
| ANGLE_SKIP_TEST_IF(!supportsPixelLocalStorage()); |
| |
| PixelLocalStoragePrototype pls; |
| |
| // Run a fibonacci loop that stores and loads the same PLS multiple times. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(fibonacci, binding=0, rgba16f); |
| void main() |
| { |
| pixelLocalStore(fibonacci, vec4(1, 0, 0, 0)); // fib(1, 0, 0, 0) |
| for (int i = 0; i < 3; ++i) |
| { |
| vec4 fib0 = pixelLocalLoad(fibonacci); |
| vec4 fib1; |
| fib1.w = fib0.x + fib0.y; |
| fib1.z = fib1.w + fib0.x; |
| fib1.y = fib1.z + fib1.w; |
| fib1.x = fib1.y + fib1.z; // fib(i*4 + (5, 4, 3, 2)) |
| pixelLocalStore(fibonacci, fib1); |
| } |
| // fib is at indices (13, 12, 11, 10) |
| })"); |
| |
| PLSTestTexture tex(GL_RGBA16F); |
| |
| GLFramebuffer fbo; |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, tex, 0, 0, W, H, GL_RGBA16F); |
| glViewport(0, 0, W, H); |
| glDrawBuffers(0, nullptr); |
| |
| glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_ZERO})); |
| drawBoxes(pls, {{FULLSCREEN}}); |
| glEndPixelLocalStorageANGLE(); |
| |
| attachTextureToScratchFBO(tex); |
| EXPECT_PIXEL_RECT32F_EQ(0, 0, W, H, GLColor32F(233, 144, 89, 55)); // fib(13, 12, 11, 10) |
| |
| ASSERT_GL_NO_ERROR(); |
| |
| // Now verify that r32f and r32ui still reload as (r, 0, 0, 1), even after an in-shader store. |
| useProgram(R"( |
| PIXEL_LOCAL_DECL(pls32f, binding=0, r32f); |
| PIXEL_LOCAL_DECL_UI(pls32ui, binding=1, r32ui); |
| out vec4 fragcolor; |
| void main() |
| { |
| pixelLocalStore(pls32f, vec4(1, .5, .5, .5)); |
| pixelLocalStore(pls32ui, uvec4(1, 1, 1, 0)); |
| if ((int(floor(gl_FragCoord.x)) & 1) == 0) |
| fragcolor = pixelLocalLoad(pls32f); |
| else |
| fragcolor = vec4(pixelLocalLoad(pls32ui)); |
| })"); |
| |
| tex.reset(GL_RGBA8); |
| glBindFramebuffer(GL_FRAMEBUFFER, fbo); |
| glFramebufferPixelLocalStorageANGLE(0, 0 /*memoryless*/, 0, 0, W, H, GL_R32F); |
| glFramebufferPixelLocalStorageANGLE(1, 0 /*memoryless*/, 0, 0, W, H, GL_R32UI); |
| glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); |
| glDrawBuffers(1, GLenumArray({GL_COLOR_ATTACHMENT0})); |
| |
| glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_ZERO, GL_ZERO})); |
| drawBoxes(pls, {{FULLSCREEN}}); |
| glEndPixelLocalStorageANGLE(); |
| |
| EXPECT_PIXEL_RECT_EQ(0, 0, W, H, GLColor(255, 0, 0, 255)); |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| ANGLE_INSTANTIATE_TEST_ES31(PixelLocalStorageTest); |