// // Copyright 2024 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. // // BlobCacheTest: // Unit tests for the GL_ANGLE_blob_cache extension. // Must be included first to prevent errors with "None". #include "test_utils/ANGLETest.h" #include #include #include "common/PackedEnums.h" #include "common/angleutils.h" #include "test_utils/MultiThreadSteps.h" #include "test_utils/gl_raii.h" #include "util/EGLWindow.h" #include "util/test_utils.h" namespace angle { constexpr char kExtName[] = "GL_ANGLE_blob_cache"; enum class CacheOpResult { SetSuccess, GetNotFound, GetMemoryTooSmall, GetSuccess, ValueNotSet, EnumCount }; angle::PackedEnumMap kCacheOpToString = { {CacheOpResult::SetSuccess, "SetSuccess"}, {CacheOpResult::GetNotFound, "GetNotFound"}, {CacheOpResult::GetMemoryTooSmall, "GetMemoryTooSmall"}, {CacheOpResult::GetSuccess, "GetSuccess"}, {CacheOpResult::ValueNotSet, "ValueNotSet"}, }; std::ostream &operator<<(std::ostream &os, CacheOpResult result) { return os << kCacheOpToString[result]; } struct TestUserData { std::map, std::vector> cache; CacheOpResult cacheOpResult = CacheOpResult::ValueNotSet; }; void GL_APIENTRY SetBlob(const void *key, GLsizeiptr keySize, const void *value, GLsizeiptr valueSize, const void *userParam) { TestUserData *data = reinterpret_cast(const_cast(userParam)); std::vector keyVec(keySize); memcpy(keyVec.data(), key, keySize); std::vector valueVec(valueSize); memcpy(valueVec.data(), value, valueSize); data->cache[keyVec] = valueVec; data->cacheOpResult = CacheOpResult::SetSuccess; } void GL_APIENTRY SetCorruptedBlob(const void *key, GLsizeiptr keySize, const void *value, GLsizeiptr valueSize, const void *userParam) { TestUserData *data = reinterpret_cast(const_cast(userParam)); std::vector keyVec(keySize); memcpy(keyVec.data(), key, keySize); std::vector valueVec(valueSize); memcpy(valueVec.data(), value, valueSize); // Corrupt the data ++valueVec[valueVec.size() / 2]; ++valueVec[valueVec.size() / 3]; ++valueVec[valueVec.size() / 4]; ++valueVec[2 * valueVec.size() / 3]; ++valueVec[3 * valueVec.size() / 4]; data->cache[keyVec] = valueVec; data->cacheOpResult = CacheOpResult::SetSuccess; } GLsizeiptr GL_APIENTRY GetBlob(const void *key, GLsizeiptr keySize, void *value, GLsizeiptr valueSize, const void *userParam) { TestUserData *data = reinterpret_cast(const_cast(userParam)); std::vector keyVec(keySize); memcpy(keyVec.data(), key, keySize); auto entry = data->cache.find(keyVec); if (entry == data->cache.end()) { // A compile+link operation can generate multiple queries to the cache; one per shader and // one for link. For the purposes of the test, make sure that any of these hitting the // cache is considered a success, particularly because it's valid for the pipeline cache // entry not to exist in the cache. if (data->cacheOpResult != CacheOpResult::GetSuccess) { data->cacheOpResult = CacheOpResult::GetNotFound; } return 0; } if (entry->second.size() <= static_cast(valueSize)) { memcpy(value, entry->second.data(), entry->second.size()); data->cacheOpResult = CacheOpResult::GetSuccess; } else { data->cacheOpResult = CacheOpResult::GetMemoryTooSmall; } return entry->second.size(); } void WaitProgramBinaryReady(GLuint program) { // Using GL_ANGLE_program_binary_readiness_query, wait for post-link tasks to finish. // Otherwise, the program binary may not yet be cached. Only needed when a |set| operation is // expected. if (!IsGLExtensionEnabled("GL_ANGLE_program_binary_readiness_query")) { return; } GLint ready = false; while (!ready) { glGetProgramiv(program, GL_PROGRAM_BINARY_READY_ANGLE, &ready); angle::Sleep(0); } } class BlobCacheTest : public ANGLETest<> { protected: BlobCacheTest() : mHasBlobCache(false) { // Force disply caching off. Blob cache functions require it. forceNewDisplay(); } void testSetUp() override { mHasBlobCache = EnsureGLExtensionEnabled(kExtName); } void testTearDown() override {} bool programBinaryAvailable() { return IsGLExtensionEnabled("GL_OES_get_program_binary"); } bool mHasBlobCache; }; // Makes sure the extension exists and works TEST_P(BlobCacheTest, Functional) { ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::CacheCompiledShader)); ANGLE_SKIP_TEST_IF(getEGLWindow()->isFeatureEnabled(Feature::DisableProgramCaching)); EXPECT_TRUE(mHasBlobCache); TestUserData data; glBlobCacheCallbacksANGLE(SetBlob, GetBlob, &data); ASSERT_EGL_SUCCESS(); constexpr char kVertexShaderSrc[] = R"(attribute vec4 aTest; attribute vec2 aPosition; varying vec4 vTest; void main() { vTest = aTest; gl_Position = vec4(aPosition, 0.0, 1.0); gl_PointSize = 1.0; })"; constexpr char kFragmentShaderSrc[] = R"(precision mediump float; varying vec4 vTest; void main() { gl_FragColor = vTest; })"; constexpr char kVertexShaderSrc2[] = R"(attribute vec4 aTest; attribute vec2 aPosition; varying vec4 vTest; void main() { vTest = aTest; gl_Position = vec4(aPosition, 1.0, 1.0); gl_PointSize = 1.0; })"; constexpr char kFragmentShaderSrc2[] = R"(precision mediump float; varying vec4 vTest; void main() { gl_FragColor = vTest - vec4(0.0, 1.0, 0.0, 0.0); })"; // Compile a shader so it puts something in the cache. Note that with Vulkan, some optional // link subtasks may run beyond link, and so the caching is delayed. An explicit wait on these // tasks is done for this reason. if (programBinaryAvailable()) { ANGLE_GL_PROGRAM(program, kVertexShaderSrc, kFragmentShaderSrc); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Compile the same shader again, so it would try to retrieve it from the cache program.makeRaster(kVertexShaderSrc, kFragmentShaderSrc); ASSERT_TRUE(program.valid()); EXPECT_EQ(CacheOpResult::GetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Compile another shader, which should create a new entry program.makeRaster(kVertexShaderSrc2, kFragmentShaderSrc2); ASSERT_TRUE(program.valid()); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Compile the first shader again, which should still reside in the cache program.makeRaster(kVertexShaderSrc, kFragmentShaderSrc); ASSERT_TRUE(program.valid()); EXPECT_EQ(CacheOpResult::GetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Make sure deleting the program doesn't result in a binary save. Regression test for a // bug where the binary was re-cached after being loaded. glUseProgram(0); program.reset(); EXPECT_EQ(CacheOpResult::ValueNotSet, data.cacheOpResult); } } // Makes sure the caching is always done without an explicit wait for post-link events (if any) TEST_P(BlobCacheTest, FunctionalWithoutWait) { ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::CacheCompiledShader)); ANGLE_SKIP_TEST_IF(getEGLWindow()->isFeatureEnabled(Feature::DisableProgramCaching)); EXPECT_TRUE(mHasBlobCache); TestUserData data; glBlobCacheCallbacksANGLE(SetBlob, GetBlob, &data); ASSERT_GL_NO_ERROR(); constexpr char kVertexShaderSrc[] = R"(attribute vec4 aTest; attribute vec2 aPosition; varying vec4 vTest; varying vec4 vTest2; void main() { vTest = aTest; vTest2 = aTest; gl_Position = vec4(aPosition, 1.0, 1.0); gl_PointSize = 1.0; })"; constexpr char kFragmentShaderSrc[] = R"(precision mediump float; varying vec4 vTest; varying vec4 vTest2; void main() { gl_FragColor = vTest + vTest2 - vec4(0.0, 1.0, 0.0, 0.0); })"; if (programBinaryAvailable()) { // Make the conditions ideal for Vulkan's warm up task to match the draw call. constexpr uint32_t kSize = 1; GLTexture color; glBindTexture(GL_TEXTURE_2D, color); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); GLFramebuffer fbo; glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0); ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); ANGLE_GL_PROGRAM(program, kVertexShaderSrc, kFragmentShaderSrc); // First, draw with the program. In the Vulkan backend, this can lead to a wait on the warm // up task since the description matches the one needed for the draw. glUseProgram(program); glDrawArrays(GL_TRIANGLE_STRIP, 0, 3); // Delete the program to make sure caching the binary can no longer be delayed. glUseProgram(0); program.reset(); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; } } // Tests error conditions of the APIs. TEST_P(BlobCacheTest, NegativeAPI) { ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::CacheCompiledShader)); ANGLE_SKIP_TEST_IF(getEGLWindow()->isFeatureEnabled(Feature::DisableProgramCaching)); EXPECT_TRUE(mHasBlobCache); // Test bad arguments glBlobCacheCallbacksANGLE(SetBlob, nullptr, nullptr); EXPECT_GL_ERROR(GL_INVALID_OPERATION); glBlobCacheCallbacksANGLE(nullptr, GetBlob, nullptr); EXPECT_GL_ERROR(GL_INVALID_OPERATION); } // Regression test for including the fragment output locations in the program key. // http://anglebug.com/42263144 TEST_P(BlobCacheTest, FragmentOutputLocationKey) { ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::CacheCompiledShader)); ANGLE_SKIP_TEST_IF(getEGLWindow()->isFeatureEnabled(Feature::DisableProgramCaching)); ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_blend_func_extended") || getClientMajorVersion() < 3); TestUserData data; glBlobCacheCallbacksANGLE(SetBlob, GetBlob, &data); ASSERT_GL_NO_ERROR(); // Compile a shader so it puts something in the cache if (programBinaryAvailable()) { glEnable(GL_SCISSOR_TEST); glScissor(0, 0, 1, 1); constexpr char kFragmentShaderSrc[] = R"(#version 300 es #extension GL_EXT_blend_func_extended : require precision mediump float; uniform vec4 src; uniform vec4 src1; out vec4 FragData; out vec4 SecondaryFragData; void main() { FragData = src; SecondaryFragData = src1; })"; constexpr char kVertexShaderSrc[] = R"(#version 300 es in vec4 position; void main() { gl_Position = position; })"; GLuint program = CompileProgram(kVertexShaderSrc, kFragmentShaderSrc, [](GLuint p) { glBindFragDataLocationEXT(p, 0, "FragData[0]"); glBindFragDataLocationIndexedEXT(p, 0, 1, "SecondaryFragData[0]"); }); ASSERT_NE(0u, program); glUseProgram(program); glDrawArrays(GL_TRIANGLES, 0, 3); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Re-link the program with different fragment output bindings program = CompileProgram(kVertexShaderSrc, kFragmentShaderSrc, [](GLuint p) { glBindFragDataLocationEXT(p, 0, "FragData"); glBindFragDataLocationIndexedEXT(p, 0, 1, "SecondaryFragData"); }); ASSERT_NE(0u, program); glUseProgram(program); glDrawArrays(GL_TRIANGLES, 0, 3); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; } } // Checks that the shader cache, which is used when this extension is available, is working // properly. TEST_P(BlobCacheTest, ShaderCacheFunctional) { ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::CacheCompiledShader)); ANGLE_SKIP_TEST_IF(getEGLWindow()->isFeatureEnabled(Feature::DisableProgramCaching)); ANGLE_SKIP_TEST_IF(!IsVulkan()); TestUserData data; glBlobCacheCallbacksANGLE(SetBlob, GetBlob, &data); ASSERT_GL_NO_ERROR(); constexpr char kVertexShaderSrc[] = R"(attribute vec4 aTest; attribute vec2 aPosition; varying vec4 vTest; void main() { vTest = aTest; gl_Position = vec4(aPosition, 0.0, 1.0); gl_PointSize = 1.0; })"; constexpr char kFragmentShaderSrc[] = R"(precision mediump float; varying vec4 vTest; void main() { gl_FragColor = vTest; })"; // Compile a shader so it puts something in the cache GLuint shaderID = CompileShader(GL_VERTEX_SHADER, kVertexShaderSrc); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); // Compile the same shader again, so it would try to retrieve it from the cache shaderID = CompileShader(GL_VERTEX_SHADER, kVertexShaderSrc); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::GetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); // Compile another shader, which should create a new entry shaderID = CompileShader(GL_FRAGMENT_SHADER, kFragmentShaderSrc); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); // Compile the first shader again, which should still reside in the cache shaderID = CompileShader(GL_VERTEX_SHADER, kVertexShaderSrc); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::GetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); } // Makes sure ANGLE recovers from corrupted cache. TEST_P(BlobCacheTest, CacheCorruption) { ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::CacheCompiledShader)); ANGLE_SKIP_TEST_IF(getEGLWindow()->isFeatureEnabled(Feature::DisableProgramCaching)); TestUserData data; glBlobCacheCallbacksANGLE(SetCorruptedBlob, GetBlob, &data); ASSERT_GL_NO_ERROR(); ANGLE_SKIP_TEST_IF(!programBinaryAvailable()); // Compile the program once and draw with it ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); const GLint colorUniformLocation = glGetUniformLocation(program, angle::essl1_shaders::ColorUniform()); ASSERT_NE(colorUniformLocation, -1); glUniform4f(colorUniformLocation, 1, 0, 0, 1); drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Compile/link the same program again, so it would try to retrieve it from the cache. GetBlob // should return success, but because the cache is corrupted by using SetCorruptedBlob, ANGLE // should redo the compile/link when Program::deserialize fails and set the blob again. program.makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); ASSERT_TRUE(program.valid()); glUseProgram(program); glUniform4f(colorUniformLocation, 0, 1, 0, 1); drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); } class BlobCacheInternalRejectionTest : public BlobCacheTest {}; // Makes sure ANGLE recovers from internal (backend) rejection of the program blob, while everything // seems fine to ANGLE. TEST_P(BlobCacheInternalRejectionTest, Functional) { TestUserData data; glBlobCacheCallbacksANGLE(SetBlob, GetBlob, &data); ASSERT_GL_NO_ERROR(); ANGLE_SKIP_TEST_IF(!programBinaryAvailable()); // Compile the program once and draw with it ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); glUseProgram(program); const GLint colorUniformLocation = glGetUniformLocation(program, angle::essl1_shaders::ColorUniform()); ASSERT_NE(colorUniformLocation, -1); glUniform4f(colorUniformLocation, 1, 0, 0, 1); drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; // Compile/link the same program again, so it would try to retrieve it from the cache. The blob // will be corrupted due to the CorruptProgramBinaryForTesting feature. GetBlob should return // success, and ANGLE would think the program is fine. After ANGLE internal updates, the // backend should reject the program binary, at which point ANGLE should redo the compile/link // and set the blob again. program.makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor()); ASSERT_TRUE(program.valid()); glUseProgram(program); glUniform4f(colorUniformLocation, 0, 1, 0, 1); drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f); EXPECT_GL_NO_ERROR(); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); WaitProgramBinaryReady(program); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); } // Makes sure ANGLE recovers from internal (backend) rejection of the shader blob, while everything // seems fine to ANGLE. TEST_P(BlobCacheInternalRejectionTest, ShaderCacheFunctional) { ANGLE_SKIP_TEST_IF(!IsVulkan()); TestUserData data; glBlobCacheCallbacksANGLE(SetBlob, GetBlob, &data); ASSERT_GL_NO_ERROR(); // Compile a shader so it puts something in the cache GLuint shaderID = CompileShader(GL_VERTEX_SHADER, essl1_shaders::vs::Simple()); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); // Compile another shader, which should create a new entry shaderID = CompileShader(GL_FRAGMENT_SHADER, essl1_shaders::fs::UniformColor()); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); // Compile the first shader again, which should still reside in the cache, but is corrupted. // The cached entry should be discarded and compilation performed again (which sets another // entry in the cache). shaderID = CompileShader(GL_VERTEX_SHADER, essl1_shaders::vs::Simple()); ASSERT_TRUE(shaderID != 0); EXPECT_EQ(CacheOpResult::SetSuccess, data.cacheOpResult); data.cacheOpResult = CacheOpResult::ValueNotSet; glDeleteShader(shaderID); } ANGLE_INSTANTIATE_TEST(BlobCacheTest, ES2_D3D9(), ES2_D3D11(), ES3_D3D11(), ES2_OPENGL(), ES3_OPENGL(), ES3_OPENGLES(), ES2_OPENGLES(), ES2_METAL(), ES3_METAL(), // Note: For the Vulkan backend, disable reads and writes for the global // pipeline cache, so it does not interfere with the test's expectations of // when the cache should and shouldn't be hit. ES2_VULKAN() .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache), ES3_VULKAN_SWIFTSHADER() .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache), ES3_VULKAN() .enable(Feature::AsyncCommandQueue) .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache), ES2_VULKAN_SWIFTSHADER() .enable(Feature::AsyncCommandQueue) .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache), ES2_VULKAN_SWIFTSHADER() .enable(Feature::EnableParallelCompileAndLink) .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache), ES3_VULKAN() .enable(Feature::EnableParallelCompileAndLink) .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache), ES2_VULKAN_SWIFTSHADER() .enable(Feature::EnableParallelCompileAndLink) .enable(Feature::AsyncCommandQueue) .enable(Feature::DisablePipelineCacheLoadForTesting) .disable(Feature::SyncMonolithicPipelinesToBlobCache)); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BlobCacheInternalRejectionTest); ANGLE_INSTANTIATE_TEST(BlobCacheInternalRejectionTest, ES2_OPENGL().enable(Feature::CorruptProgramBinaryForTesting), ES2_OPENGLES().enable(Feature::CorruptProgramBinaryForTesting)); } // namespace angle