diff --git a/src/tests/egl_tests/EGLContextSharingTest.cpp b/src/tests/egl_tests/EGLContextSharingTest.cpp index 3f1ee22d24..22de433d3b 100644 --- a/src/tests/egl_tests/EGLContextSharingTest.cpp +++ b/src/tests/egl_tests/EGLContextSharingTest.cpp @@ -411,6 +411,234 @@ TEST_P(EGLContextSharingTest, DisplayShareGroupReleaseShareGroupThatOwnsStagedUp eglDestroyContext(display, context2); } +// Regression test for a Vulkan-backend lifetime bug: a per-ShareGroup SamplerCache +// entry can be destroyed (vkDestroySampler) while a submitted command buffer in a +// *different* ShareGroup still has a COMBINED_IMAGE_SAMPLER descriptor referencing +// that VkSampler. SamplerHelper carries no ResourceUse, so neither +// SamplerCache::destroy() nor Renderer::cleanupOrphanedSamplers() gates destruction +// on queue-serial completion. With Vulkan validation layers enabled this fires +// VUID-vkDestroySampler-sampler-01082. +TEST_P(EGLContextSharingTest, DisplayShareGroupSamplerInFlightWhenOwningShareGroupDestroyed) +{ + EGLDisplay display = getEGLWindow()->getDisplay(); + ANGLE_SKIP_TEST_IF( + !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")); + ANGLE_SKIP_TEST_IF(!IsVulkan()); + + EGLConfig config = getEGLWindow()->getConfig(); + EGLSurface surface = getEGLWindow()->getSurface(); + + const EGLint inShareGroupContextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; + + // Two contexts in the *display* texture share group but each in its own + // egl::ShareGroup / ShareGroupVk (no share_context passed). + EGLContext contextA = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); + EGLContext contextB = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); + ASSERT_NE(contextA, EGL_NO_CONTEXT); + ASSERT_NE(contextB, EGL_NO_CONTEXT); + + // A larger render target widens the GPU window for the heavy draw in B. + constexpr int kHeavyDim = 256; + const EGLint pbufferAttribs[] = {EGL_WIDTH, kHeavyDim, EGL_HEIGHT, kHeavyDim, EGL_NONE}; + EGLSurface pbufferB = eglCreatePbufferSurface(display, config, pbufferAttribs); + ASSERT_NE(pbufferB, EGL_NO_SURFACE); + + // ---- Context A: create the shared texture and draw with it once. This + // populates TextureVk::mSampler from A's per-ShareGroup SamplerCache (S_A). + ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, contextA)); + GLuint sharedTex = 0; + glGenTextures(1, &sharedTex); + glBindTexture(GL_TEXTURE_2D, sharedTex); + constexpr GLsizei kTexSize = 4; + std::vector texData(kTexSize * kTexSize, GLColor::green); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, kTexSize, kTexSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, + texData.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + { + ANGLE_GL_PROGRAM(progA, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); + drawQuad(progA, essl1_shaders::PositionAttrib(), 0.5f); + EXPECT_GL_NO_ERROR(); + glFlush(); + } + + // ---- Context B: bind the *same* display-shared texture and submit a long + // running draw. TextureVk::syncState early-outs (mSampler already set, no + // dirty bits), so the descriptor set written for this draw references S_A, + // even though S_A came from A's ShareGroup cache. + ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB)); + glViewport(0, 0, kHeavyDim, kHeavyDim); + glBindTexture(GL_TEXTURE_2D, sharedTex); + + constexpr char kHeavyFS[] = R"(precision highp float; +varying vec2 v_texCoord; +uniform sampler2D u_tex2D; +void main() +{ + vec4 acc = vec4(0.0); + // Heavy per-fragment loop that samples the shared texture each iteration so + // S_A is actively dereferenced for the lifetime of this draw on the GPU. + for (int i = 0; i < 4000; ++i) + { + acc += texture2D(u_tex2D, v_texCoord + vec2(float(i) * 0.000001, 0.0)); + } + gl_FragColor = acc * 0.00025; +})"; + ANGLE_GL_PROGRAM(progHeavy, essl1_shaders::vs::Texture2D(), kHeavyFS); + glUseProgram(progHeavy); + glUniform1i(glGetUniformLocation(progHeavy, "u_tex2D"), 0); + // Several heavy full-surface draws back-to-back to make the submitted batch + // take measurable wall-clock time even on fast software rasterizers. + for (int i = 0; i < 16; ++i) + { + drawQuad(progHeavy, essl1_shaders::PositionAttrib(), 0.5f); + } + EXPECT_GL_NO_ERROR(); + glFlush(); // batch_B submitted, GPU now executing with S_A in its descriptor set. + + // ---- Still in B: dirty a sampler-state bit and draw again so that + // TextureVk::syncState() takes the resetSampler() path, dropping the + // texture's reference on S_A. After this S_A's only CPU reference is the + // entry in A's SamplerCache. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + { + ANGLE_GL_PROGRAM(progB2, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); + drawQuad(progB2, essl1_shaders::PositionAttrib(), 0.5f); + EXPECT_GL_NO_ERROR(); + glFlush(); + } + + // ---- Destroy context A (and with it ShareGroupVk A). ContextVk::onDestroy + // waits only for A's own submitted serials, *not* batch_B. Then + // ShareGroupVk::onDestroy → SamplerCache::destroy(/*orphan*/true): S_A is + // unique() so it is destroyed immediately via mPayload.clear() → + // vkDestroySampler(S_A). If batch_B is still in flight, VVL reports + // VUID-vkDestroySampler-sampler-01082. + SafeDestroyContext(display, contextA); + + // Drain B so ASAN / VVL has flushed all messages before teardown. + ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB)); + glFinish(); + glDeleteTextures(1, &sharedTex); + EXPECT_GL_NO_ERROR(); + + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroySurface(display, pbufferB); + SafeDestroyContext(display, contextB); + getEGLWindow()->makeCurrent(); +} + +// Variant that follows the orphan-list path explicitly: A is destroyed first +// (sampler is moved to Renderer::mOrphanedSamplers because the texture still +// holds it), then B drops the last reference and a third context's destruction +// drives Renderer::cleanupGarbage() → cleanupOrphanedSamplers() while batch_B +// is still on the GPU. +TEST_P(EGLContextSharingTest, DisplayShareGroupOrphanedSamplerReapedWhileInFlight) +{ + EGLDisplay display = getEGLWindow()->getDisplay(); + ANGLE_SKIP_TEST_IF( + !IsEGLDisplayExtensionEnabled(display, "EGL_ANGLE_display_texture_share_group")); + ANGLE_SKIP_TEST_IF(!IsVulkan()); + + EGLConfig config = getEGLWindow()->getConfig(); + EGLSurface surface = getEGLWindow()->getSurface(); + + const EGLint inShareGroupContextAttribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_TRUE, EGL_NONE}; + + EGLContext contextA = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); + EGLContext contextB = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); + EGLContext contextC = eglCreateContext(display, config, nullptr, inShareGroupContextAttribs); + ASSERT_NE(contextA, EGL_NO_CONTEXT); + ASSERT_NE(contextB, EGL_NO_CONTEXT); + ASSERT_NE(contextC, EGL_NO_CONTEXT); + + constexpr int kHeavyDim = 256; + const EGLint pbufferAttribs[] = {EGL_WIDTH, kHeavyDim, EGL_HEIGHT, kHeavyDim, EGL_NONE}; + EGLSurface pbufferB = eglCreatePbufferSurface(display, config, pbufferAttribs); + ASSERT_NE(pbufferB, EGL_NO_SURFACE); + + // A: create T, draw → mSampler = S_A. + ASSERT_EGL_TRUE(eglMakeCurrent(display, surface, surface, contextA)); + GLuint sharedTex = 0; + glGenTextures(1, &sharedTex); + glBindTexture(GL_TEXTURE_2D, sharedTex); + std::vector texData(16, GLColor::cyan); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texData.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + { + ANGLE_GL_PROGRAM(p, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); + drawQuad(p, essl1_shaders::PositionAttrib(), 0.5f); + EXPECT_GL_NO_ERROR(); + glFlush(); + } + + // B: heavy draw with T → batch_B references S_A. + ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB)); + glViewport(0, 0, kHeavyDim, kHeavyDim); + glBindTexture(GL_TEXTURE_2D, sharedTex); + constexpr char kHeavyFS[] = R"(precision highp float; +varying vec2 v_texCoord; +uniform sampler2D u_tex2D; +void main() +{ + vec4 acc = vec4(0.0); + for (int i = 0; i < 4000; ++i) + { + acc += texture2D(u_tex2D, v_texCoord + vec2(float(i) * 0.000001, 0.0)); + } + gl_FragColor = acc * 0.00025; +})"; + ANGLE_GL_PROGRAM(progHeavy, essl1_shaders::vs::Texture2D(), kHeavyFS); + glUseProgram(progHeavy); + glUniform1i(glGetUniformLocation(progHeavy, "u_tex2D"), 0); + for (int i = 0; i < 16; ++i) + { + drawQuad(progHeavy, essl1_shaders::PositionAttrib(), 0.5f); + } + EXPECT_GL_NO_ERROR(); + glFlush(); + + // Destroy A first → S_A is *not* unique (TextureVk still holds it), so it is + // moved onto Renderer::mOrphanedSamplers. refcount: T + orphan = 2. + SafeDestroyContext(display, contextA); + + // B (current again after the destroy dance): dirty sampler state, draw → + // resetSampler() → S_A refcount drops to 1 (orphan list only). + ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB)); + glBindTexture(GL_TEXTURE_2D, sharedTex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + { + ANGLE_GL_PROGRAM(p2, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D()); + drawQuad(p2, essl1_shaders::PositionAttrib(), 0.5f); + EXPECT_GL_NO_ERROR(); + glFlush(); + } + + // Destroying C runs ContextVk::onDestroy → finishImpl → clearAllGarbage → + // Renderer::cleanupGarbage → cleanupOrphanedSamplers. S_A.unique() ⇒ + // erased ⇒ vkDestroySampler(S_A) with no queue-serial gate, while batch_B + // is still in flight → VUID-vkDestroySampler-sampler-01082. + SafeDestroyContext(display, contextC); + + ASSERT_EGL_TRUE(eglMakeCurrent(display, pbufferB, pbufferB, contextB)); + glFinish(); + glDeleteTextures(1, &sharedTex); + EXPECT_GL_NO_ERROR(); + + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroySurface(display, pbufferB); + SafeDestroyContext(display, contextB); + getEGLWindow()->makeCurrent(); +} + // Tests that after creating a texture using EGL_ANGLE_display_texture_share_group, // and use it for sampling, and then deleting the Context (which destroys shareGroup) works. If // anything cached in ShareGroup, it should be handled nicely if texture can outlive ShareGroup (for